diff --git a/IHP/IDE/CodeGen/ActionGenerator.hs b/IHP/IDE/CodeGen/ActionGenerator.hs
--- a/IHP/IDE/CodeGen/ActionGenerator.hs
+++ b/IHP/IDE/CodeGen/ActionGenerator.hs
@@ -3,8 +3,7 @@
 import IHP.Prelude
 import qualified Data.Text as Text
 import IHP.IDE.CodeGen.Types
-import qualified IHP.IDE.SchemaDesigner.Parser as SchemaDesigner
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import qualified IHP.IDE.CodeGen.ViewGenerator as ViewGenerator
 import Text.Countable (pluralize)
 
@@ -20,9 +19,7 @@
     if (null actionName || null controllerName)
         then pure $ Left "Neither action name nor controller name can be empty"
         else do
-            schema <- SchemaDesigner.parseSchemaSql >>= \case
-                Left parserError -> pure []
-                Right statements -> pure statements
+            schema <- loadAppSchema
             let actionConfig = ActionConfig {controllerName, applicationName, modelName, actionName }
             let actionPlan = generateGenericAction schema actionConfig doGenerateView
             if doGenerateView
@@ -49,12 +46,7 @@
             controllerName = config.controllerName
             name = ucfirst $ config.actionName
             singularName = config.modelName
-            nameWithSuffix = if "Action" `isSuffixOf` name
-                    then name
-                    else name <> "Action" -- e.g. TestAction
-            viewName = if "Action" `isSuffixOf` name
-                then Text.dropEnd 6 name
-                else name
+            (nameWithSuffix, viewName) = ensureSuffix "Action" name
             indexAction = pluralize singularName <> "Action"
             specialCases = [
                   (indexAction, indexContent)
@@ -79,63 +71,26 @@
             idFieldName = lcfirst singularName <> "Id"
             idType = "Id " <> singularName
             model = ucfirst singularName
+
+            tableFound = [ modelNameToTableName modelVariableSingular, modelVariableSingular ]
+                    |> mapMaybe (columnsForTable schema)
+                    |> headMay
+                    |> isJust
+
+            actionBodyConfig = ActionBodyConfig { singularName, modelVariableSingular, idFieldName, model, indexAction = name <> "Action", tableFound }
+
             indexContent =
                 ""
                 <> "    action " <> name <> "Action = do\n"
                 <> "        " <> modelVariablePlural <> " <- query @" <> model <> " |> fetch\n"
                 <> "        render IndexView { .. }\n"
 
-            newContent =
-                ""
-                <> "    action New" <> singularName <> "Action = do\n"
-                <> "        let " <> modelVariableSingular <> " = newRecord\n"
-                <> "        render NewView { .. }\n"
-
-            showContent =
-                ""
-                <> "    action Show" <> singularName <> "Action { " <> idFieldName <> " } = do\n"
-                <> "        " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n"
-                <> "        render ShowView { .. }\n"
-
-            editContent =
-                ""
-                <> "    action Edit" <> singularName <> "Action { " <> idFieldName <> " } = do\n"
-                <> "        " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n"
-                <> "        render EditView { .. }\n"
-
-            updateContent =
-                ""
-                <> "    action Update" <> singularName <> "Action { " <> idFieldName <> " } = do\n"
-                <> "        " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n"
-                <> "        " <> modelVariableSingular <> "\n"
-                <> "            |> build" <> singularName <> "\n"
-                <> "            |> ifValid \\case\n"
-                <> "                Left " <> modelVariableSingular <> " -> render EditView { .. }\n"
-                <> "                Right " <> modelVariableSingular <> " -> do\n"
-                <> "                    " <> modelVariableSingular <> " <- " <> modelVariableSingular <> " |> updateRecord\n"
-                <> "                    setSuccessMessage \"" <> model <> " updated\"\n"
-                <> "                    redirectTo Edit" <> singularName <> "Action { .. }\n"
-
-            createContent =
-                ""
-                <> "    action Create" <> singularName <> "Action = do\n"
-                <> "        let " <> modelVariableSingular <> " = newRecord @"  <> model <> "\n"
-                <> "        " <> modelVariableSingular <> "\n"
-                <> "            |> build" <> singularName <> "\n"
-                <> "            |> ifValid \\case\n"
-                <> "                Left " <> modelVariableSingular <> " -> render NewView { .. }\n"
-                <> "                Right " <> modelVariableSingular <> " -> do\n"
-                <> "                    " <> modelVariableSingular <> " <- " <> modelVariableSingular <> " |> createRecord\n"
-                <> "                    setSuccessMessage \"" <> model <> " created\"\n"
-                <> "                    redirectTo " <> name <> "Action\n"
-
-            deleteContent =
-                ""
-                <> "    action Delete" <> singularName <> "Action { " <> idFieldName <> " } = do\n"
-                <> "        " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n"
-                <> "        deleteRecord " <> modelVariableSingular <> "\n"
-                <> "        setSuccessMessage \"" <> model <> " deleted\"\n"
-                <> "        redirectTo " <> name <> "Action\n"
+            newContent = generateNewActionBody actionBodyConfig
+            showContent = generateShowActionBody actionBodyConfig
+            editContent = generateEditActionBody actionBodyConfig
+            updateContent = generateUpdateActionBody actionBodyConfig
+            createContent = generateCreateActionBody actionBodyConfig
+            deleteContent = generateDeleteActionBody actionBodyConfig
 
             typesContentGeneric =
                    "    | " <> nameWithSuffix
@@ -150,6 +105,6 @@
                 else typesContentWithParameter
 
         in
-            [ AddAction { filePath = config.applicationName <> "/Controller/" <> controllerName <> ".hs", fileContent = chosenContent}
-            , AddToDataConstructor { dataConstructor = "data " <> controllerName, filePath = config.applicationName <> "/Types.hs", fileContent = chosenType }
+            [ AddAction { filePath = textToOsPath (config.applicationName <> "/Controller/" <> controllerName <> ".hs"), fileContent = chosenContent}
+            , AddToDataConstructor { dataConstructor = "data " <> controllerName, filePath = textToOsPath (config.applicationName <> "/Types.hs"), fileContent = chosenType }
             ]
diff --git a/IHP/IDE/CodeGen/ApplicationGenerator.hs b/IHP/IDE/CodeGen/ApplicationGenerator.hs
--- a/IHP/IDE/CodeGen/ApplicationGenerator.hs
+++ b/IHP/IDE/CodeGen/ApplicationGenerator.hs
@@ -47,7 +47,6 @@
                 <> "instance InitControllerContext " <> applicationName <> "Application where\n"
                 <> "    initContext = do\n"
                 <> "        setLayout defaultLayout\n"
-                <> "        initAutoRefresh\n"
             controllerPreludeHs =
                 "module " <> applicationName <> ".Controller.Prelude\n"
                 <> "( module " <> applicationName <> ".Types\n"
@@ -68,7 +67,6 @@
                 <> "import IHP.ViewPrelude\n"
                 <> "import IHP.Environment\n"
                 <> "import Generated.Types\n"
-                <> "import IHP.Controller.RequestContext\n"
                 <> "import " <> applicationName <> ".Types\n"
                 <> "import " <> applicationName <> ".Routes\n"
                 <> "import Application.Helper.View\n"
@@ -100,7 +98,7 @@
                 <> "\n"
                 <> "stylesheets :: Html\n"
                 <> "stylesheets = [hsx|\n"
-                <> "        <link rel=\"stylesheet\" href={assetPath \"/vendor/bootstrap-5.2.1/bootstrap.min.css\"}/>\n"
+                <> "        <link rel=\"stylesheet\" href={assetPath \"/vendor/bootstrap-5.3.8/bootstrap.min.css\"}/>\n"
                 <> "        <link rel=\"stylesheet\" href={assetPath \"/vendor/flatpickr.min.css\"}/>\n"
                 <> "        <link rel=\"stylesheet\" href={assetPath \"/app.css\"}/>\n"
                 <> "    |]\n"
@@ -108,10 +106,10 @@
                 <> "scripts :: Html\n"
                 <> "scripts = [hsx|\n"
                 <> "        {when isDevelopment devScripts}\n"
-                <> "        <script src={assetPath \"/vendor/jquery-3.6.0.slim.min.js\"}></script>\n"
+                <> "        <script src={assetPath \"/vendor/jquery-4.0.0.slim.min.js\"}></script>\n"
                 <> "        <script src={assetPath \"/vendor/timeago.js\"}></script>\n"
                 <> "        <script src={assetPath \"/vendor/popper-2.11.6.min.js\"}></script>\n"
-                <> "        <script src={assetPath \"/vendor/bootstrap-5.2.1/bootstrap.min.js\"}></script>\n"
+                <> "        <script src={assetPath \"/vendor/bootstrap-5.3.8/bootstrap.min.js\"}></script>\n"
                 <> "        <script src={assetPath \"/vendor/flatpickr.js\"}></script>\n"
                 <> "        <script src={assetPath \"/vendor/morphdom-umd.min.js\"}></script>\n"
                 <> "        <script src={assetPath \"/vendor/turbolinks.js\"}></script>\n"
@@ -207,19 +205,19 @@
              <>"|]"
 
         in
-            [ EnsureDirectory { directory = applicationName }
-            , EnsureDirectory { directory = applicationName <> "/Controller" }
-            , EnsureDirectory { directory = applicationName <> "/View" }
-            , EnsureDirectory { directory = applicationName <> "/View/Static" }
+            [ EnsureDirectory { directory = textToOsPath applicationName }
+            , EnsureDirectory { directory = textToOsPath (applicationName <> "/Controller") }
+            , EnsureDirectory { directory = textToOsPath (applicationName <> "/View") }
+            , EnsureDirectory { directory = textToOsPath (applicationName <> "/View/Static") }
             , AddImport  { filePath = "Main.hs", fileContent = "import " <> applicationName <> ".FrontController" }
             , AddImport  { filePath = "Main.hs", fileContent = "import " <> applicationName <> ".Types" }
             , AddMountToFrontController { filePath = "Main.hs", applicationName = applicationName }
-            , CreateFile { filePath = applicationName <> "/Types.hs", fileContent = typesHs }
-            , CreateFile { filePath = applicationName <> "/Routes.hs", fileContent = routesHs }
-            , CreateFile { filePath = applicationName <> "/FrontController.hs", fileContent = frontControllerHs }
-            , CreateFile { filePath = applicationName <> "/Controller/Prelude.hs", fileContent = controllerPreludeHs }
-            , CreateFile { filePath = applicationName <> "/View/Layout.hs", fileContent = viewLayoutHs }
-            , CreateFile { filePath = applicationName <> "/View/Prelude.hs", fileContent = viewPreludeHs }
-            , CreateFile { filePath = applicationName <> "/Controller/Static.hs", fileContent = welcomeControllerStaticHs }
-            , CreateFile { filePath = applicationName <> "/View/Static/Welcome.hs", fileContent = welcomeViewStaticHs }
+            , CreateFile { filePath = textToOsPath (applicationName <> "/Types.hs"), fileContent = typesHs }
+            , CreateFile { filePath = textToOsPath (applicationName <> "/Routes.hs"), fileContent = routesHs }
+            , CreateFile { filePath = textToOsPath (applicationName <> "/FrontController.hs"), fileContent = frontControllerHs }
+            , CreateFile { filePath = textToOsPath (applicationName <> "/Controller/Prelude.hs"), fileContent = controllerPreludeHs }
+            , CreateFile { filePath = textToOsPath (applicationName <> "/View/Layout.hs"), fileContent = viewLayoutHs }
+            , CreateFile { filePath = textToOsPath (applicationName <> "/View/Prelude.hs"), fileContent = viewPreludeHs }
+            , CreateFile { filePath = textToOsPath (applicationName <> "/Controller/Static.hs"), fileContent = welcomeControllerStaticHs }
+            , CreateFile { filePath = textToOsPath (applicationName <> "/View/Static/Welcome.hs"), fileContent = welcomeViewStaticHs }
             ]
diff --git a/IHP/IDE/CodeGen/Controller.hs b/IHP/IDE/CodeGen/Controller.hs
--- a/IHP/IDE/CodeGen/Controller.hs
+++ b/IHP/IDE/CodeGen/Controller.hs
@@ -20,11 +20,11 @@
 import IHP.IDE.CodeGen.JobGenerator as JobGenerator
 import IHP.IDE.ToolServer.Helper.Controller
 import qualified System.Process as Process
-import qualified System.Directory as Directory
+import qualified System.Directory.OsPath as Directory
 import qualified Data.Text as Text
 import qualified Data.Text.IO as Text
 import qualified Text.Inflections as Inflector
-import System.Directory
+import System.OsPath (decodeUtf)
 
 instance Controller CodeGenController where
     action GeneratorsAction = do
@@ -34,28 +34,64 @@
         let controllerName = paramOrDefault "" "name"
         let applicationName = paramOrDefault "Web" "applicationName"
         let pagination = paramOrDefault False "pagination"
+        let indexAction = paramOrDefault True "indexAction"
+        let newAction = paramOrDefault True "newAction"
+        let showAction = paramOrDefault True "showAction"
+        let createAction = paramOrDefault True "createAction"
+        let editAction = paramOrDefault True "editAction"
+        let updateAction = paramOrDefault True "updateAction"
+        let deleteAction = paramOrDefault True "deleteAction"
         controllerAlreadyExists <- doesControllerExist controllerName applicationName
         applications <- findApplications
         when controllerAlreadyExists do
             setErrorMessage "Controller with this name does already exist."
             redirectTo NewControllerAction
-        plan <- ControllerGenerator.buildPlan controllerName applicationName pagination
+        let config = ControllerGenerator.defaultControllerConfig
+                { ControllerGenerator.applicationName = applicationName
+                , ControllerGenerator.paginationEnabled = pagination
+                , ControllerGenerator.indexActionEnabled = indexAction
+                , ControllerGenerator.newActionEnabled = newAction
+                , ControllerGenerator.showActionEnabled = showAction
+                , ControllerGenerator.createActionEnabled = createAction
+                , ControllerGenerator.editActionEnabled = editAction
+                , ControllerGenerator.updateActionEnabled = updateAction
+                , ControllerGenerator.deleteActionEnabled = deleteAction
+                }
+        plan <- ControllerGenerator.buildPlan controllerName config
         render NewControllerView { .. }
         where
-            doesControllerExist controllerName applicationName = doesFileExist $ cs applicationName <> "/Controller/" <> cs controllerName <> ".hs"
+            doesControllerExist controllerName applicationName = Directory.doesFileExist $ textToOsPath $ applicationName <> "/Controller/" <> controllerName <> ".hs"
 
     action CreateControllerAction = do
         let controllerName = param "name"
         let applicationName = param "applicationName"
         let pagination = paramOrDefault False "pagination"
-        (Right plan) <- ControllerGenerator.buildPlan controllerName applicationName pagination
+        let indexAction = paramOrDefault True "indexAction"
+        let newAction = paramOrDefault True "newAction"
+        let showAction = paramOrDefault True "showAction"
+        let createAction = paramOrDefault True "createAction"
+        let editAction = paramOrDefault True "editAction"
+        let updateAction = paramOrDefault True "updateAction"
+        let deleteAction = paramOrDefault True "deleteAction"
+        let config = ControllerGenerator.defaultControllerConfig
+                { ControllerGenerator.applicationName = applicationName
+                , ControllerGenerator.paginationEnabled = pagination
+                , ControllerGenerator.indexActionEnabled = indexAction
+                , ControllerGenerator.newActionEnabled = newAction
+                , ControllerGenerator.showActionEnabled = showAction
+                , ControllerGenerator.createActionEnabled = createAction
+                , ControllerGenerator.editActionEnabled = editAction
+                , ControllerGenerator.updateActionEnabled = updateAction
+                , ControllerGenerator.deleteActionEnabled = deleteAction
+                }
+        (Right plan) <- ControllerGenerator.buildPlan controllerName config
         executePlan plan
         setSuccessMessage "Controller generated"
         redirectTo GeneratorsAction
 
     action NewScriptAction = do
         let scriptName = paramOrDefault "" "name"
-        scriptAlreadyExists <- doesFileExist $ "Application/Script/" <> cs scriptName <> ".hs"
+        scriptAlreadyExists <- Directory.doesFileExist $ textToOsPath $ "Application/Script/" <> scriptName <> ".hs"
         when scriptAlreadyExists do
             setErrorMessage "Script with this name already exists."
             redirectTo NewScriptAction
@@ -73,7 +109,7 @@
         let viewName = paramOrDefault "" "name"
         let applicationName = paramOrDefault "Web" "applicationName"
         let controllerName = paramOrDefault "" "controllerName"
-        viewAlreadyExists <- doesFileExist $ (cs applicationName) <> "/View/" <> (cs controllerName) <> "/" <> (cs viewName) <>".hs"
+        viewAlreadyExists <- Directory.doesFileExist $ textToOsPath $ applicationName <> "/View/" <> controllerName <> "/" <> viewName <> ".hs"
         when viewAlreadyExists do
             setErrorMessage "View with this name already exists."
             redirectTo NewViewAction
@@ -95,7 +131,7 @@
         let mailName = paramOrDefault "" "name"
         let applicationName = paramOrDefault "Web" "applicationName"
         let controllerName = paramOrDefault "" "controllerName"
-        mailAlreadyExists <- doesFileExist $ (cs applicationName) <> "/Mail/" <> (cs controllerName) <> "/" <> (cs mailName) <>".hs"
+        mailAlreadyExists <- Directory.doesFileExist $ textToOsPath $ applicationName <> "/Mail/" <> controllerName <> "/" <> mailName <> ".hs"
         when mailAlreadyExists do
             setErrorMessage "Mail with this name already exists."
             redirectTo NewMailAction
@@ -174,34 +210,38 @@
 executePlan actions = forEach actions evalAction
     where
         evalAction CreateFile { filePath, fileContent } = do
-            Text.writeFile (cs filePath) (cs fileContent)
-            putStrLn ("+ " <> filePath)
+            fp <- decodeUtf filePath
+            Text.writeFile fp (cs fileContent)
+            putStrLn ("+ " <> osPathToText filePath)
         evalAction AppendToFile { filePath, fileContent } = do
-            Text.appendFile (cs filePath) fileContent
-            putStrLn ("* " <> filePath)
+            fp <- decodeUtf filePath
+            Text.appendFile fp fileContent
+            putStrLn ("* " <> osPathToText filePath)
         evalAction AppendToMarker { marker, filePath, fileContent } = do
-            content <- Text.readFile (cs filePath)
+            fp <- decodeUtf filePath
+            content <- Text.readFile fp
             let newContent = Text.replace marker (marker <> "\n" <> cs fileContent) (cs content)
-            Text.writeFile (cs filePath) (cs newContent)
-            putStrLn ("* " <> filePath <> " (import)")
+            Text.writeFile fp (cs newContent)
+            putStrLn ("* " <> osPathToText filePath <> " (import)")
         evalAction AddImport { filePath, fileContent } = do
             addImport filePath fileContent
-            putStrLn ("* " <> filePath <> " (import)")
+            putStrLn ("* " <> osPathToText filePath <> " (import)")
         evalAction AddAction { filePath, fileContent } = do
             addAction filePath [fileContent]
-            putStrLn ("* " <> filePath <> " (AddAction)")
+            putStrLn ("* " <> osPathToText filePath <> " (AddAction)")
         evalAction AddMountToFrontController { filePath, applicationName } = do
             addMountControllerStatement filePath applicationName
-            putStrLn ("* " <> filePath <> " (AddMountToFrontController)")
+            putStrLn ("* " <> osPathToText filePath <> " (AddMountToFrontController)")
         evalAction AddToDataConstructor { dataConstructor, filePath, fileContent } = do
-            content <- Text.readFile (cs filePath)
+            fp <- decodeUtf filePath
+            content <- Text.readFile fp
             case addToDataConstructor content dataConstructor fileContent of
                 Just newContent -> do
-                    Text.writeFile (cs filePath) (cs newContent)
-                    putStrLn ("* " <> filePath <> " (AddToDataConstructor)")
-                Nothing -> putStrLn ("Could not automatically add " <> tshow content <> " to " <> filePath)
+                    Text.writeFile fp (cs newContent)
+                    putStrLn ("* " <> osPathToText filePath <> " (AddToDataConstructor)")
+                Nothing -> putStrLn ("Could not automatically add " <> tshow content <> " to " <> osPathToText filePath)
         evalAction EnsureDirectory { directory } = do
-            Directory.createDirectoryIfMissing True (cs directory)
+            Directory.createDirectoryIfMissing True directory
         evalAction RunShellCommand { shellCommand } = do
             _ <- Process.system (cs shellCommand)
             putStrLn ("* " <> shellCommand)
@@ -209,41 +249,46 @@
 undoPlan :: [GeneratorAction] -> IO()
 undoPlan actions = forEach actions evalAction
     where
-        evalAction CreateFile { filePath, fileContent } = do
-            (Directory.removeFile (cs filePath)) `catch` handleError
-            putStrLn ("- " <> filePath)
+        evalAction CreateFile { filePath } = do
+            Directory.removeFile filePath `catch` handleError
+            putStrLn ("- " <> osPathToText filePath)
         evalAction AppendToFile { filePath, fileContent } = do
-            deleteTextFromFile (cs filePath) fileContent `catch` handleError
-            putStrLn ("* " <> filePath)
-        evalAction AppendToMarker { marker, filePath, fileContent } = do
-            (deleteTextFromFile (cs filePath) (fileContent <> "\n")) `catch` handleError
-            putStrLn ("* " <> filePath <> " (import)")
+            deleteTextFromFile filePath fileContent `catch` handleError
+            putStrLn ("* " <> osPathToText filePath)
+        evalAction AppendToMarker { filePath, fileContent } = do
+            deleteTextFromFile filePath (fileContent <> "\n") `catch` handleError
+            putStrLn ("* " <> osPathToText filePath <> " (import)")
         evalAction AddImport { filePath, fileContent } = do
-            (deleteTextFromFile (cs filePath) (fileContent <> "\n")) `catch` handleError
-            putStrLn ("* " <> filePath <> " (import)")
+            deleteTextFromFile filePath (fileContent <> "\n") `catch` handleError
+            putStrLn ("* " <> osPathToText filePath <> " (import)")
         evalAction AddAction { filePath, fileContent } = do
-            (deleteTextFromFile (cs filePath) (fileContent <> "\n")) `catch` handleError
-            putStrLn ("* " <> filePath <> " (RemoveAction)")
-        evalAction AddToDataConstructor { dataConstructor, filePath, fileContent } = do
-            (deleteTextFromFile (cs filePath) (fileContent <> "\n")) `catch` handleError
-            putStrLn ("* " <> filePath <> " (RemoveFromDataConstructor)")
+            deleteTextFromFile filePath (fileContent <> "\n") `catch` handleError
+            putStrLn ("* " <> osPathToText filePath <> " (RemoveAction)")
+        evalAction AddMountToFrontController { filePath, applicationName } = do
+            -- Undo is best-effort; removing the mount statement is not straightforward
+            putStrLn ("* " <> osPathToText filePath <> " (RemoveMountFromFrontController - manual removal may be needed)")
+        evalAction AddToDataConstructor { filePath, fileContent } = do
+            deleteTextFromFile filePath (fileContent <> "\n") `catch` handleError
+            putStrLn ("* " <> osPathToText filePath <> " (RemoveFromDataConstructor)")
         evalAction EnsureDirectory { directory } = do
-            (Directory.removeDirectory (cs directory)) `catch` handleError
+            Directory.removeDirectory directory `catch` handleError
         evalAction RunShellCommand { shellCommand } = pure ()
         handleError :: SomeException -> IO ()
         handleError ex = putStrLn (tshow ex)
 
-deleteTextFromFile :: Text -> Text -> IO ()
+deleteTextFromFile :: OsPath -> Text -> IO ()
 deleteTextFromFile filePath lineContent = do
-    fileContent <- Text.readFile (cs filePath)
+    fp <- decodeUtf filePath
+    fileContent <- Text.readFile fp
     let replacedContent = Text.replace lineContent "" fileContent
-    Text.writeFile (cs filePath) replacedContent
+    Text.writeFile fp replacedContent
 
-addImport :: Text -> Text -> IO ()
+addImport :: OsPath -> Text -> IO ()
 addImport file importStatement = do
-    content :: Text <- Text.readFile (cs file)
+    fp <- decodeUtf file
+    content :: Text <- Text.readFile fp
     case addImport' content importStatement of
-        Just newContent -> Text.writeFile (cs file) (cs newContent)
+        Just newContent -> Text.writeFile fp (cs newContent)
         Nothing -> pure ()
     pure ()
 
@@ -253,23 +298,25 @@
         then Nothing
         else appendLineAfter content ("import" `isPrefixOf`) [importStatement]
 
-addAction :: Text -> [Text] -> IO ()
+addAction :: OsPath -> [Text] -> IO ()
 addAction filePath fileContent = do
-    content <- Text.readFile (cs filePath)
+    fp <- decodeUtf filePath
+    content <- Text.readFile fp
     case addAction' content fileContent of
-        Just newContent -> Text.writeFile (cs filePath) (cs newContent)
-        Nothing -> putStrLn ("Could not automatically add " <> tshow content <> " to " <> filePath)
+        Just newContent -> Text.writeFile fp (cs newContent)
+        Nothing -> putStrLn ("Could not automatically add " <> tshow content <> " to " <> osPathToText filePath)
     pure ()
 
 addAction' :: Text -> [Text] -> Maybe Text
 addAction' fileContent = appendLineAfter fileContent ("instance Controller" `isPrefixOf`)
 
-addMountControllerStatement :: Text -> Text -> IO ()
+addMountControllerStatement :: OsPath -> Text -> IO ()
 addMountControllerStatement file applicationName = do
-    content :: Text <- Text.readFile (cs file)
+    fp <- decodeUtf file
+    content :: Text <- Text.readFile fp
     case addMountControllerStatement' applicationName content of
-        Just newContent -> Text.writeFile (cs file) (cs newContent)
-        Nothing -> putStrLn ("Could not automatically add " <> tshow applicationName <> " to " <> file)
+        Just newContent -> Text.writeFile fp (cs newContent)
+        Nothing -> putStrLn ("Could not automatically add " <> tshow applicationName <> " to " <> osPathToText file)
     pure ()
 
 addMountControllerStatement' :: Text -> Text -> Maybe Text
diff --git a/IHP/IDE/CodeGen/ControllerGenerator.hs b/IHP/IDE/CodeGen/ControllerGenerator.hs
--- a/IHP/IDE/CodeGen/ControllerGenerator.hs
+++ b/IHP/IDE/CodeGen/ControllerGenerator.hs
@@ -1,35 +1,33 @@
-module IHP.IDE.CodeGen.ControllerGenerator (buildPlan, buildPlan') where
+module IHP.IDE.CodeGen.ControllerGenerator (buildPlan, buildPlan', ControllerConfig(..), defaultControllerConfig) where
 
 import ClassyPrelude
+import IHP.Prelude (textToOsPath)
 import IHP.NameSupport
 import IHP.HaskellSupport
 import qualified Data.Text as Text
 import qualified Data.Char as Char
-import qualified IHP.IDE.SchemaDesigner.Parser as SchemaDesigner
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.CodeGen.Types
 import qualified IHP.IDE.CodeGen.ViewGenerator as ViewGenerator
 import Text.Countable (singularize, pluralize)
 
-buildPlan :: Text -> Text -> Bool -> IO (Either Text [GeneratorAction])
-buildPlan rawControllerName applicationName paginationEnabled = do
-    schema <- SchemaDesigner.parseSchemaSql >>= \case
-        Left parserError -> pure []
-        Right statements -> pure statements
+buildPlan :: Text -> ControllerConfig -> IO (Either Text [GeneratorAction])
+buildPlan rawControllerName config = do
+    schema <- loadAppSchema
     let controllerName = tableNameToControllerName rawControllerName
     let modelName = tableNameToModelName rawControllerName
-    pure $ Right $ buildPlan' schema applicationName controllerName modelName paginationEnabled
+    pure $ Right $ buildPlan' schema config { controllerName, modelName }
 
-buildPlan' schema applicationName controllerName modelName paginationEnabled =
+buildPlan' :: [Statement] -> ControllerConfig -> [GeneratorAction]
+buildPlan' schema config =
     let
-        config = ControllerConfig { modelName, controllerName, applicationName, paginationEnabled }
-        viewPlans = generateViews schema applicationName controllerName paginationEnabled
+        viewPlans = generateViews schema config
     in
-        [ CreateFile { filePath = applicationName <> "/Controller/" <> controllerName <> ".hs", fileContent = (generateController schema config) }
-        , AppendToFile { filePath = applicationName <> "/Routes.hs", fileContent = "\n" <> (controllerInstance config) }
-        , AppendToFile { filePath = applicationName <> "/Types.hs", fileContent = (generateControllerData config) }
-        , AppendToMarker { marker = "-- Controller Imports", filePath = applicationName <> "/FrontController.hs", fileContent = ("import " <> applicationName <> ".Controller." <> controllerName) }
-        , AppendToMarker { marker = "-- Generator Marker", filePath = applicationName <> "/FrontController.hs", fileContent = ("        , parseRoute @" <> controllerName <> "Controller") }
+        [ CreateFile { filePath = textToOsPath (config.applicationName <> "/Controller/" <> config.controllerName <> ".hs"), fileContent = (generateController schema config) }
+        , AppendToFile { filePath = textToOsPath (config.applicationName <> "/Routes.hs"), fileContent = "\n" <> (controllerInstance config) }
+        , AppendToFile { filePath = textToOsPath (config.applicationName <> "/Types.hs"), fileContent = (generateControllerData schema config) }
+        , AppendToMarker { marker = "-- Controller Imports", filePath = textToOsPath (config.applicationName <> "/FrontController.hs"), fileContent = ("import " <> config.applicationName <> ".Controller." <> config.controllerName) }
+        , AppendToMarker { marker = "-- Generator Marker", filePath = textToOsPath (config.applicationName <> "/FrontController.hs"), fileContent = ("        , parseRoute @" <> config.controllerName <> "Controller") }
         ]
         <> viewPlans
 
@@ -38,33 +36,71 @@
     , applicationName :: Text
     , modelName :: Text
     , paginationEnabled :: Bool
+    , indexActionEnabled :: Bool
+    , newActionEnabled :: Bool
+    , showActionEnabled :: Bool
+    , createActionEnabled :: Bool
+    , editActionEnabled :: Bool
+    , updateActionEnabled :: Bool
+    , deleteActionEnabled :: Bool
     } deriving (Eq, Show)
 
+defaultControllerConfig :: ControllerConfig
+defaultControllerConfig = ControllerConfig
+    { controllerName = ""
+    , applicationName = "Web"
+    , modelName = ""
+    , paginationEnabled = False
+    , indexActionEnabled = True
+    , newActionEnabled = True
+    , showActionEnabled = True
+    , createActionEnabled = True
+    , editActionEnabled = True
+    , updateActionEnabled = True
+    , deleteActionEnabled = True
+    }
+
 controllerInstance :: ControllerConfig -> Text
 controllerInstance ControllerConfig { controllerName, modelName, applicationName } =
     "instance AutoRoute " <> controllerName <> "Controller\n\n"
 
 data HaskellModule = HaskellModule { moduleName :: Text, body :: Text }
 
-generateControllerData :: ControllerConfig -> Text
-generateControllerData config =
+generateControllerData :: [Statement] -> ControllerConfig -> Text
+generateControllerData schema config =
     let
         name = config.controllerName
         pluralName = config.controllerName |> lcfirst |> pluralize |> ucfirst
         singularName = config.modelName |> lcfirst |> singularize |> ucfirst
+        modelVariableSingular = lcfirst singularName
         idFieldName = lcfirst singularName <> "Id"
         idType = "Id " <> singularName
+        tableFound = [ modelNameToTableName modelVariableSingular, modelVariableSingular ]
+                |> mapMaybe (columnsForTable schema)
+                |> headMay
+                |> isJust
+        idRecord suffix = if tableFound
+            then suffix <> " { " <> idFieldName <> " :: !(" <> idType <> ") }"
+            else suffix
+        constructors = catMaybes
+            [ if config.indexActionEnabled then Just (pluralName <> "Action") else Nothing
+            , if config.newActionEnabled then Just ("New" <> singularName <> "Action") else Nothing
+            , if config.showActionEnabled then Just (idRecord $ "Show" <> singularName <> "Action") else Nothing
+            , if config.createActionEnabled then Just ("Create" <> singularName <> "Action") else Nothing
+            , if config.editActionEnabled then Just (idRecord $ "Edit" <> singularName <> "Action") else Nothing
+            , if config.updateActionEnabled then Just (idRecord $ "Update" <> singularName <> "Action") else Nothing
+            , if config.deleteActionEnabled then Just (idRecord $ "Delete" <> singularName <> "Action") else Nothing
+            ]
+        formattedConstructors = case constructors of
+            (first:rest) ->
+                "    = " <> first <> "\n"
+                <> concatMap (\c -> "    | " <> c <> "\n") rest
+                <> "    deriving (Eq, Show, Data)\n"
+            [] -> "    deriving (Eq, Show, Data)\n"
     in
         "\n"
         <> "data " <> name <> "Controller\n"
-        <> "    = " <> pluralName <> "Action\n"
-        <> "    | New" <> singularName <> "Action\n"
-        <> "    | Show" <> singularName <> "Action { " <> idFieldName <> " :: !(" <> idType <> ") }\n"
-        <> "    | Create" <> singularName <> "Action\n"
-        <> "    | Edit" <> singularName <> "Action { " <> idFieldName <> " :: !(" <> idType <> ") }\n"
-        <> "    | Update" <> singularName <> "Action { " <> idFieldName <> " :: !(" <> idType <> ") }\n"
-        <> "    | Delete" <> singularName <> "Action { " <> idFieldName <> " :: !(" <> idType <> ") }\n"
-        <> "    deriving (Eq, Show, Data)\n"
+        <> formattedConstructors
 
 generateController :: [Statement] -> ControllerConfig -> Text
 generateController schema config =
@@ -76,13 +112,18 @@
         moduleName =  applicationName <> ".Controller." <> name
         controllerName = name <> "Controller"
 
-        importStatements =
-            [ "import " <> applicationName <> ".Controller.Prelude"
-            , "import " <> qualifiedViewModuleName config "Index"
-            , "import " <> qualifiedViewModuleName config "New"
-            , "import " <> qualifiedViewModuleName config "Edit"
-            , "import " <> qualifiedViewModuleName config "Show"
+        -- Create/Update reference NewView/EditView on validation errors
+        needsNewView = config.newActionEnabled || config.createActionEnabled
+        needsEditView = config.editActionEnabled || config.updateActionEnabled
+        needsIndexView = config.indexActionEnabled
+        needsShowView = config.showActionEnabled
 
+        importStatements = catMaybes
+            [ Just ("import " <> applicationName <> ".Controller.Prelude")
+            , if needsIndexView then Just ("import " <> qualifiedViewModuleName config "Index") else Nothing
+            , if needsNewView then Just ("import " <> qualifiedViewModuleName config "New") else Nothing
+            , if needsEditView then Just ("import " <> qualifiedViewModuleName config "Edit") else Nothing
+            , if needsShowView then Just ("import " <> qualifiedViewModuleName config "Show") else Nothing
             ]
 
         modelVariablePlural = lcfirst name
@@ -91,6 +132,8 @@
         model = ucfirst singularName
         paginationEnabled = config.paginationEnabled
 
+        actionBodyConfig = ActionBodyConfig { singularName, modelVariableSingular, idFieldName, model, indexAction = pluralName <> "Action", tableFound }
+
         indexAction =
             ""
             <> "    action " <> pluralName <> "Action = do\n"
@@ -101,70 +144,92 @@
             )
             <> "        render IndexView { .. }\n"
 
-        newAction =
-            ""
-            <> "    action New" <> singularName <> "Action = do\n"
-            <> "        let " <> modelVariableSingular <> " = newRecord\n"
-            <> "        render NewView { .. }\n"
-
-        showAction =
-            ""
-            <> "    action Show" <> singularName <> "Action { " <> idFieldName <> " } = do\n"
-            <> "        " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n"
-            <> "        render ShowView { .. }\n"
+        newAction = generateNewActionBody actionBodyConfig
+        showAction = generateShowActionBody actionBodyConfig
+        editAction = generateEditActionBody actionBodyConfig
 
-        editAction =
-            ""
-            <> "    action Edit" <> singularName <> "Action { " <> idFieldName <> " } = do\n"
-            <> "        " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n"
-            <> "        render EditView { .. }\n"
+        tableFound :: Bool
+        tableFound = [ modelNameToTableName modelVariableSingular, modelVariableSingular ]
+                |> mapMaybe (columnsForTable schema)
+                |> headMay
+                |> isJust
 
-        modelFields :: [Text]
-        modelFields = [ modelNameToTableName modelVariableSingular, modelVariableSingular ]
-                |> mapMaybe (fieldsForTable schema)
+        modelColumns :: [Column]
+        modelColumns = [ modelNameToTableName modelVariableSingular, modelVariableSingular ]
+                |> mapMaybe (columnsForTable schema)
                 |> headMay
                 |> fromMaybe []
 
-        updateAction =
-            ""
-            <> "    action Update" <> singularName <> "Action { " <> idFieldName <> " } = do\n"
-            <> "        " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n"
-            <> "        " <> modelVariableSingular <> "\n"
-            <> "            |> build" <> singularName <> "\n"
-            <> "            |> ifValid \\case\n"
-            <> "                Left " <> modelVariableSingular <> " -> render EditView { .. }\n"
-            <> "                Right " <> modelVariableSingular <> " -> do\n"
-            <> "                    " <> modelVariableSingular <> " <- " <> modelVariableSingular <> " |> updateRecord\n"
-            <> "                    setSuccessMessage \"" <> model <> " updated\"\n"
-            <> "                    redirectTo Edit" <> singularName <> "Action { .. }\n"
+        modelFields :: [Text]
+        modelFields = modelColumns
+                |> map (.name)
+                |> map columnNameToFieldName
 
-        createAction =
-            ""
-            <> "    action Create" <> singularName <> "Action = do\n"
-            <> "        let " <> modelVariableSingular <> " = newRecord @"  <> model <> "\n"
-            <> "        " <> modelVariableSingular <> "\n"
-            <> "            |> build" <> singularName <> "\n"
-            <> "            |> ifValid \\case\n"
-            <> "                Left " <> modelVariableSingular <> " -> render NewView { .. } \n"
-            <> "                Right " <> modelVariableSingular <> " -> do\n"
-            <> "                    " <> modelVariableSingular <> " <- " <> modelVariableSingular <> " |> createRecord\n"
-            <> "                    setSuccessMessage \"" <> model <> " created\"\n"
-            <> "                    redirectTo " <> pluralName <> "Action\n"
+        foreignKeys :: [(Text, Text)]
+        foreignKeys = [ modelNameToTableName modelVariableSingular, modelVariableSingular ]
+                |> map (foreignKeysForTable schema)
+                |> concat
 
-        deleteAction =
-            ""
-            <> "    action Delete" <> singularName <> "Action { " <> idFieldName <> " } = do\n"
-            <> "        " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n"
-            <> "        deleteRecord " <> modelVariableSingular <> "\n"
-            <> "        setSuccessMessage \"" <> model <> " deleted\"\n"
-            <> "        redirectTo " <> pluralName <> "Action\n"
+        extraUniqueColumns :: [Text]
+        extraUniqueColumns = [ modelNameToTableName modelVariableSingular, modelVariableSingular ]
+                |> map (uniqueColumnsForTable schema)
+                |> concat
 
+        updateAction = generateUpdateActionBody actionBodyConfig
+        createAction = generateCreateActionBody actionBodyConfig
+        deleteAction = generateDeleteActionBody actionBodyConfig
+
+        needsBuildModel = config.createActionEnabled || config.updateActionEnabled
+
         fromParams =
-            ""
-            <> "build" <> singularName <> " " <> modelVariableSingular <> " = " <> modelVariableSingular <> "\n"
-            <> "    |> fill " <> toTypeLevelList modelFields <> "\n"
+            if needsBuildModel
+            then ""
+                <> "build" <> singularName <> " " <> modelVariableSingular <> " = " <> modelVariableSingular <> "\n"
+                <> "    |> fill " <> toTypeLevelList modelFields <> "\n"
+                <> validationLines
+            else ""
 
         toTypeLevelList values = "@'" <> (values |> tshow |> Text.replace "," ", ")
+
+        validationLines :: Text
+        validationLines =
+            let
+                fkColumnNames = map fst foreignKeys
+                isTextLike = isTextLikeType . (.columnType)
+                isTextLikeType PText = True
+                isTextLikeType (PVaryingN _) = True
+                isTextLikeType (PCharacterN _) = True
+                isTextLikeType _ = False
+
+                validationsFor col =
+                    let fieldName = columnNameToFieldName col.name
+                        nonEmptyLine
+                            | col.notNull && isTextLike col && col.name `notElem` fkColumnNames
+                            = ["    |> validateField #" <> fieldName <> " nonEmpty"]
+                            | otherwise = []
+                        emailLine
+                            | "email" `Text.isSuffixOf` col.name && isTextLike col
+                            = ["    |> validateField #" <> fieldName <> " isEmail"]
+                            | otherwise = []
+                        uniqueLine
+                            | col.name `elem` extraUniqueColumns
+                            = ["    -- TODO: |> validateIsUnique #" <> fieldName]
+                            | otherwise = []
+                    in nonEmptyLine <> emailLine <> uniqueLine
+            in
+                case concatMap validationsFor modelColumns of
+                    [] -> ""
+                    lines -> intercalate "\n" lines <> "\n"
+
+        actionBodies = catMaybes
+            [ if config.indexActionEnabled then Just indexAction else Nothing
+            , if config.newActionEnabled then Just newAction else Nothing
+            , if config.showActionEnabled then Just showAction else Nothing
+            , if config.editActionEnabled then Just editAction else Nothing
+            , if config.updateActionEnabled then Just updateAction else Nothing
+            , if config.createActionEnabled then Just createAction else Nothing
+            , if config.deleteActionEnabled then Just deleteAction else Nothing
+            ]
     in
         ""
         <> "module " <> moduleName <> " where" <> "\n"
@@ -172,45 +237,40 @@
         <> intercalate "\n" importStatements
         <> "\n\n"
         <> "instance Controller " <> controllerName <> " where\n"
-        <> indexAction
-        <> "\n"
-        <> newAction
-        <> "\n"
-        <> showAction
-        <> "\n"
-        <> editAction
-        <> "\n"
-        <> updateAction
-        <> "\n"
-        <> createAction
-        <> "\n"
-        <> deleteAction
+        <> intercalate "\n" actionBodies
         <> "\n"
         <> fromParams
 
 -- E.g. qualifiedViewModuleName config "Edit" == "Web.View.Users.Edit"
 qualifiedViewModuleName :: ControllerConfig -> Text -> Text
 qualifiedViewModuleName config viewName =
-    config.applicationName <> ".View." <> config.controllerName <> "." <> viewName
+    qualifiedModuleName config.applicationName "View" config.controllerName viewName
 
 pathToModuleName :: Text -> Text
 pathToModuleName moduleName = Text.replace "." "/" moduleName
 
-generateViews :: [Statement] -> Text -> Text -> Bool -> [GeneratorAction]
-generateViews schema applicationName controllerName' paginationEnabled =
-    if null controllerName'
+generateViews :: [Statement] -> ControllerConfig -> [GeneratorAction]
+generateViews schema config =
+    if null config.controllerName
         then []
         else do
-            let indexPlan = ViewGenerator.buildPlan' schema (config "IndexView")
-            let newPlan = ViewGenerator.buildPlan' schema (config "NewView")
-            let showPlan = ViewGenerator.buildPlan' schema (config "ShowView")
-            let editPlan = ViewGenerator.buildPlan' schema (config "EditView")
-            indexPlan <> newPlan <> showPlan <> editPlan
+            let needsIndexView = config.indexActionEnabled
+            let needsNewView = config.newActionEnabled || config.createActionEnabled
+            let needsShowView = config.showActionEnabled
+            let needsEditView = config.editActionEnabled || config.updateActionEnabled
+            concat $ catMaybes
+                [ if needsIndexView then Just (ViewGenerator.buildPlan' schema (viewConfig "IndexView")) else Nothing
+                , if needsNewView then Just (ViewGenerator.buildPlan' schema (viewConfig "NewView")) else Nothing
+                , if needsShowView then Just (ViewGenerator.buildPlan' schema (viewConfig "ShowView")) else Nothing
+                , if needsEditView then Just (ViewGenerator.buildPlan' schema (viewConfig "EditView")) else Nothing
+                ]
     where
-        config viewName = do
-            let modelName = tableNameToModelName controllerName'
-            let controllerName = tableNameToControllerName controllerName'
-            ViewGenerator.ViewConfig { .. }
+        viewConfig viewName =
+            let modelName = config.modelName
+                controllerName = config.controllerName
+                applicationName = config.applicationName
+                paginationEnabled = config.paginationEnabled
+            in ViewGenerator.ViewConfig { .. }
 
 
 isAlphaOnly :: Text -> Bool
diff --git a/IHP/IDE/CodeGen/DefaultUuidFunction.hs b/IHP/IDE/CodeGen/DefaultUuidFunction.hs
new file mode 100644
--- /dev/null
+++ b/IHP/IDE/CodeGen/DefaultUuidFunction.hs
@@ -0,0 +1,3 @@
+module IHP.IDE.CodeGen.DefaultUuidFunction (defaultUuidFunction) where
+
+import IHP.PGVersion (defaultUuidFunction)
diff --git a/IHP/IDE/CodeGen/JobGenerator.hs b/IHP/IDE/CodeGen/JobGenerator.hs
--- a/IHP/IDE/CodeGen/JobGenerator.hs
+++ b/IHP/IDE/CodeGen/JobGenerator.hs
@@ -3,18 +3,21 @@
 import IHP.Prelude
 import qualified Data.Text as Text
 import IHP.IDE.CodeGen.Types
-import qualified System.Directory as Directory
+import qualified System.Directory.OsPath as Directory
 
 data JobConfig = JobConfig
     { applicationName :: Text
     , tableName :: Text -- E.g. create_container_jobs
     , modelName :: Text -- E.g. CreateContainerJob
     , isFirstJobInApplication :: Bool -- If true, creates Worker.hs in application directory
+    , uuidFunction :: Text -- E.g. "uuid_generate_v4" or "uuidv7"
     } deriving (Eq, Show)
 
 buildPlan :: Text -> Text -> IO (Either Text [GeneratorAction])
 buildPlan jobName applicationName = do
-    isFirstJobInApplication <- not <$> Directory.doesFileExist (cs $ applicationName <> "/Worker.hs")
+    let workerPath = textToOsPath (applicationName <> "/Worker.hs")
+    isFirstJobInApplication <- not <$> Directory.doesFileExist workerPath
+    uuidFunction <- defaultUuidFunction
     if null jobName
         then pure $ Left "Job name cannot be empty"
         else do
@@ -23,10 +26,11 @@
                     , tableName = jobName
                     , modelName = tableNameToModelName jobName
                     , isFirstJobInApplication
+                    , uuidFunction
                     }
             pure $ Right $ buildPlan' jobConfig
 
--- E.g. qualifiedMailModuleName config "Confirmation" == "Web.Mail.Users.Confirmation"
+-- E.g. qualifiedJobModuleName config == "Web.Job.CreateContainer"
 qualifiedJobModuleName :: JobConfig -> Text
 qualifiedJobModuleName config =
     config.applicationName <> ".Job." <> unqualifiedJobModuleName config
@@ -39,12 +43,7 @@
         let
             name = config.modelName
             tableName = modelNameToTableName nameWithSuffix
-            nameWithSuffix = if "Job" `isSuffixOf` name
-                then name
-                else name <> "Job" --e.g. "Test" -> "TestJob"
-            nameWithoutSuffix = if "Job" `isSuffixOf` name
-                then Text.replace "Job" "" name
-                else name --e.g. "TestJob" -> "Test""
+            (nameWithSuffix, nameWithoutSuffix) = ensureSuffix "Job" name
 
             job =
                 ""
@@ -58,7 +57,7 @@
             schemaSql =
                 ""
                 <> "CREATE TABLE " <> tableName <> " (\n"
-                <> "    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n"
+                <> "    id UUID DEFAULT " <> config.uuidFunction <> "() PRIMARY KEY NOT NULL,\n"
                 <> "    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n"
                 <> "    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n"
                 <> "    status JOB_STATUS DEFAULT 'job_status_not_started' NOT NULL,\n"
@@ -91,13 +90,13 @@
         ]
 |]
         in
-            [ EnsureDirectory { directory = config.applicationName <> "/Job" }
+            [ EnsureDirectory { directory = textToOsPath (config.applicationName <> "/Job") }
             , AppendToFile { filePath = "Application/Schema.sql", fileContent = schemaSql }
-            , CreateFile { filePath = config.applicationName <> "/Job/" <> nameWithoutSuffix <> ".hs", fileContent = job }
+            , CreateFile { filePath = textToOsPath (config.applicationName <> "/Job/" <> nameWithoutSuffix <> ".hs"), fileContent = job }
             ]
             <> if config.isFirstJobInApplication
-                    then [ CreateFile { filePath = config.applicationName <> "/Worker.hs", fileContent = emptyWorkerHs } ]
+                    then [ CreateFile { filePath = textToOsPath (config.applicationName <> "/Worker.hs"), fileContent = emptyWorkerHs } ]
                     else
-                        [ AddImport { filePath = config.applicationName <> "/Worker.hs", fileContent = "import " <> qualifiedJobModuleName config }
-                        , AppendToMarker { marker = "-- Generator Marker", filePath = config.applicationName <> "/Worker.hs", fileContent = "        , worker @" <> nameWithSuffix }
+                        [ AddImport { filePath = textToOsPath (config.applicationName <> "/Worker.hs"), fileContent = "import " <> qualifiedJobModuleName config }
+                        , AppendToMarker { marker = "-- Generator Marker", filePath = textToOsPath (config.applicationName <> "/Worker.hs"), fileContent = "        , worker @" <> nameWithSuffix }
                         ]
diff --git a/IHP/IDE/CodeGen/MailGenerator.hs b/IHP/IDE/CodeGen/MailGenerator.hs
--- a/IHP/IDE/CodeGen/MailGenerator.hs
+++ b/IHP/IDE/CodeGen/MailGenerator.hs
@@ -2,10 +2,8 @@
 
 import IHP.Prelude
 import IHP.IDE.CodeGen.Types
-import qualified Data.Text as Text
-import qualified IHP.IDE.SchemaDesigner.Parser as SchemaDesigner
-import IHP.IDE.SchemaDesigner.Types
-import Text.Countable (singularize, pluralize)
+import IHP.Postgres.Types
+import Text.Countable (pluralize)
 
 data MailConfig = MailConfig
     { controllerName :: Text
@@ -19,18 +17,16 @@
     if (null mailName || null controllerName')
         then pure $ Left "Neither mail name nor controller name can be empty"
         else do
-            schema <- SchemaDesigner.parseSchemaSql >>= \case
-                Left parserError -> pure []
-                Right statements -> pure statements
+            schema <- loadAppSchema
             let modelName = tableNameToModelName controllerName'
             let controllerName = tableNameToControllerName controllerName'
             let viewConfig = MailConfig { .. }
             pure $ Right $ buildPlan' schema viewConfig
 
 -- E.g. qualifiedMailModuleName config "Confirmation" == "Web.Mail.Users.Confirmation"
-qualifiedViewModuleName :: MailConfig -> Text -> Text
-qualifiedViewModuleName config mailName =
-    config.applicationName <> ".Mail." <> config.controllerName <> "." <> ucfirst mailName
+qualifiedMailModuleName :: MailConfig -> Text -> Text
+qualifiedMailModuleName config mailName =
+    qualifiedModuleName config.applicationName "Mail" config.controllerName (ucfirst mailName)
 
 buildPlan' :: [Statement] -> MailConfig -> [GeneratorAction]
 buildPlan' schema config =
@@ -40,12 +36,7 @@
             singularName = config.modelName
             singularVariableName = lcfirst singularName
             pluralVariableName = lcfirst controllerName
-            nameWithSuffix = if "Mail" `isSuffixOf` name
-                then name
-                else name <> "Mail" --e.g. "Test" -> "TestMail"
-            nameWithoutSuffix = if "Mail" `isSuffixOf` name
-                then Text.replace "Mail" "" name
-                else name --e.g. "TestMail" -> "Test"
+            (nameWithSuffix, nameWithoutSuffix) = ensureSuffix "Mail" name
 
             indexAction = pluralize singularName <> "Action"
 
@@ -57,7 +48,7 @@
 
             mail =
                 ""
-                <> "module " <> qualifiedViewModuleName config nameWithoutSuffix <> " where\n"
+                <> "module " <> qualifiedMailModuleName config nameWithoutSuffix <> " where\n"
                 <> "import " <> config.applicationName <> ".View.Prelude\n"
                 <> "import IHP.MailPrelude\n"
                 <> "\n"
@@ -71,7 +62,7 @@
                 <> "        Hello World\n"
                 <> "    |]\n"
         in
-            [ EnsureDirectory { directory = config.applicationName <> "/Mail/" <> controllerName }
-            , CreateFile { filePath = config.applicationName <> "/Mail/" <> controllerName <> "/" <> nameWithoutSuffix <> ".hs", fileContent = mail }
-            , AddImport { filePath = config.applicationName <> "/Controller/" <> controllerName <> ".hs", fileContent = "import " <> qualifiedViewModuleName config nameWithoutSuffix }
+            [ EnsureDirectory { directory = textToOsPath (config.applicationName <> "/Mail/" <> controllerName) }
+            , CreateFile { filePath = textToOsPath (config.applicationName <> "/Mail/" <> controllerName <> "/" <> nameWithoutSuffix <> ".hs"), fileContent = mail }
+            , AddImport { filePath = textToOsPath (config.applicationName <> "/Controller/" <> controllerName <> ".hs"), fileContent = "import " <> qualifiedMailModuleName config nameWithoutSuffix }
             ]
diff --git a/IHP/IDE/CodeGen/MigrationGenerator.hs b/IHP/IDE/CodeGen/MigrationGenerator.hs
--- a/IHP/IDE/CodeGen/MigrationGenerator.hs
+++ b/IHP/IDE/CodeGen/MigrationGenerator.hs
@@ -12,10 +12,11 @@
 import qualified IHP.NameSupport as NameSupport
 import qualified Data.Char as Char
 import qualified System.Process as Process
-import qualified IHP.IDE.SchemaDesigner.Parser as Parser
-import IHP.IDE.SchemaDesigner.Types
+import qualified IHP.Postgres.Parser as Parser
+import qualified IHP.SchemaCompiler.Parser as SchemaDesignerParser
+import IHP.Postgres.Types
 import Text.Megaparsec
-import IHP.IDE.SchemaDesigner.Compiler (compileSql)
+import IHP.Postgres.Compiler (compileSql)
 import IHP.IDE.CodeGen.Types
 import qualified IHP.FrameworkConfig as FrameworkConfig
 import Paths_ihp_ide (getDataFileName)
@@ -42,11 +43,11 @@
                 else compileSql appDiff
     pure (revision,
             [ EnsureDirectory { directory = "Application/Migration" }
-            , CreateFile { filePath = "Application/Migration/" <> migrationFile, fileContent = migrationSql }
+            , CreateFile { filePath = textToOsPath ("Application/Migration/" <> migrationFile), fileContent = migrationSql }
             ])
 
 diffAppDatabase includeIHPSchema databaseUrl = do
-    (Right schemaSql) <- Parser.parseSchemaSql
+    (Right schemaSql) <- SchemaDesignerParser.parseSchemaSql
     (Right ihpSchemaSql) <- if includeIHPSchema
             then parseIHPSchema
             else pure (Right [])
@@ -61,8 +62,11 @@
     ihpSchemaSql <- findIHPSchemaSql
     Parser.parseSqlFile ihpSchemaSql
 
-findIHPSchemaSql :: IO FilePath
-findIHPSchemaSql = getDataFileName "IHPSchema.sql"
+findIHPSchemaSql :: IO OsPath
+findIHPSchemaSql = do
+    fp <- getDataFileName ("IHPSchema.sql" :: String)
+    let fpText :: Text = cs fp
+    pure (textToOsPath fpText)
 
 diffSchemas :: [Statement] -> [Statement] -> [Statement]
 diffSchemas targetSchema' actualSchema' = (drop <> create)
@@ -134,6 +138,7 @@
                             to = createTable'.name
                         in
                             (RenameTable { from, to }):(applyRenameTable (fixIdentifiers from to (delete createTable statements)))
+                    Just _ -> s:(applyRenameTable statements)  -- Not a StatementCreateTable, skip rename
                     Nothing -> s:(applyRenameTable statements)
             where
                 createTable :: Maybe Statement
@@ -150,6 +155,7 @@
                 actualTable' :: CreateTable
                 actualTable' = case actualTable of
                     StatementCreateTable { unsafeGetCreateTable = table } -> table
+                    _ -> error "diffSchemas: expected StatementCreateTable"
 
                 fixIdentifiers :: Text -> Text -> [Statement] -> [Statement]
                 fixIdentifiers tableFrom tableTo statements = map fixIdentifier statements
@@ -339,6 +345,7 @@
                                 normalizeCol col = col { notNull = False, defaultValue = Just (VarExpression "null") }
                 applyToggleNull (statement:rest) = statement:(applyToggleNull rest)
                 applyToggleNull [] = []
+migrateTable _ _ = error "migrateTable: expected StatementCreateTable"
 
 migrateEnum :: Statement -> Statement -> [Statement]
 migrateEnum CreateEnumType { name, values = targetValues } CreateEnumType { values = actualValues } = map addValue newValues
@@ -348,6 +355,7 @@
 
         addValue :: Text -> Statement
         addValue value = AddValueToEnumType { enumName = name, newValue = value, ifNotExists = True }
+migrateEnum _ _ = error "migrateEnum: expected CreateEnumType"
 
 getAppDBSchema :: Text -> IO [Statement]
 getAppDBSchema databaseUrl = do
@@ -519,6 +527,7 @@
         unqualifiedName name = name
 normalizeExpression (DotExpression a b) = DotExpression (normalizeExpression a) b
 normalizeExpression (ExistsExpression a) = ExistsExpression (normalizeExpression a)
+normalizeExpression (InArrayExpression exprs) = InArrayExpression (map normalizeExpression exprs)
 
 -- | Replaces @table.field@ with just @field@
 --
@@ -554,6 +563,7 @@
             in
                 SelectExpression Select { columns = (recurse <$> columns), from = from, whereClause = recurse whereClause, alias }
         doUnqualify (ExistsExpression a) = ExistsExpression (doUnqualify a)
+        doUnqualify (InArrayExpression exprs) = InArrayExpression (map doUnqualify exprs)
         doUnqualify (DotExpression (VarExpression scope') b) | scope == scope' = VarExpression b
         doUnqualify (DotExpression a b) = DotExpression (doUnqualify a) b
 
@@ -585,6 +595,8 @@
         e@(SelectExpression Select { columns, from, whereClause, alias }) -> SelectExpression Select { columns = rec <$> columns, from = rec from, whereClause = rec whereClause, alias = alias }
         e@(DotExpression a b) -> DotExpression (rec a) b
         e@(ExistsExpression a) -> ExistsExpression (rec a)
+        e@(ConcatenationExpression a b) -> ConcatenationExpression (rec a) (rec b)
+        e@(InArrayExpression exprs) -> InArrayExpression (map rec exprs)
 resolveAlias Nothing fromExpression expression = expression
 
 normalizeSqlType :: PostgresType -> PostgresType
@@ -600,7 +612,7 @@
                     CreateFile {} -> True
                     otherwise     -> False
                 |> \case
-                    Just CreateFile { filePath } -> Just filePath
+                    Just CreateFile { filePath } -> Just (osPathToText filePath)
                     otherwise                    -> Nothing
         in
             path
@@ -663,6 +675,7 @@
                                     |> isNothing
                             Nothing -> True
                     )
+                Just _ -> True  -- Not a CreateIndex, keep it
                 Nothing -> True
         isImplicitlyDeleted (DropConstraint { tableName = constraintTableName }) = constraintTableName /= dropTableName
         isImplicitlyDeleted (DropPolicy { tableName = policyTableName }) = not (isNothing dropColumnName && policyTableName == dropTableName)
@@ -682,6 +695,7 @@
         (dropTableName, dropColumnName) = case dropStatement of
             DropTable { tableName } -> (tableName, Nothing)
             DropColumn { tableName, columnName } -> (tableName, Just columnName)
+            _ -> error "removeImplicitDeletions: unexpected statement type"  -- Guard ensures only DropTable/DropColumn reach here
 removeImplicitDeletions actualSchema (statement:rest) = statement:(removeImplicitDeletions actualSchema rest)
 removeImplicitDeletions actualSchema [] = []
 
diff --git a/IHP/IDE/CodeGen/ScriptGenerator.hs b/IHP/IDE/CodeGen/ScriptGenerator.hs
--- a/IHP/IDE/CodeGen/ScriptGenerator.hs
+++ b/IHP/IDE/CodeGen/ScriptGenerator.hs
@@ -7,11 +7,11 @@
 buildPlan scriptName =
     if null scriptName
         then Left "Script name cannot be empty"
-        else do 
-            let filePath = "Application/Script/" <> cs scriptName <> ".hs"
+        else do
+            let filePath = textToOsPath ("Application/Script/" <> cs scriptName <> ".hs")
             let fileContent = renderScript scriptName
             Right [ CreateFile { filePath, fileContent }
-                  , RunShellCommand { shellCommand = "chmod +x " <> filePath }
+                  , RunShellCommand { shellCommand = "chmod +x " <> osPathToText filePath }
                   ]
 
 renderScript :: Text -> Text
diff --git a/IHP/IDE/CodeGen/Types.hs b/IHP/IDE/CodeGen/Types.hs
--- a/IHP/IDE/CodeGen/Types.hs
+++ b/IHP/IDE/CodeGen/Types.hs
@@ -1,17 +1,20 @@
-module IHP.IDE.CodeGen.Types where
+module IHP.IDE.CodeGen.Types (module IHP.IDE.CodeGen.Types, defaultUuidFunction) where
 
 import IHP.Prelude
-import IHP.IDE.SchemaDesigner.Types
+import qualified Data.Text as Text
+import IHP.Postgres.Types
+import qualified IHP.SchemaCompiler.Parser as SchemaDesigner
+import IHP.IDE.CodeGen.DefaultUuidFunction (defaultUuidFunction)
 
 data GeneratorAction
-    = CreateFile { filePath :: Text, fileContent :: Text }
-    | AppendToFile { filePath :: Text, fileContent :: Text }
-    | AppendToMarker { marker :: Text, filePath :: Text, fileContent :: Text }
-    | AddImport { filePath :: Text, fileContent :: Text }
-    | AddAction { filePath :: Text, fileContent :: Text }
-    | AddToDataConstructor { dataConstructor :: Text, filePath :: Text, fileContent :: Text }
-    | AddMountToFrontController { filePath :: Text, applicationName :: Text }
-    | EnsureDirectory { directory :: Text }
+    = CreateFile { filePath :: OsPath, fileContent :: Text }
+    | AppendToFile { filePath :: OsPath, fileContent :: Text }
+    | AppendToMarker { marker :: Text, filePath :: OsPath, fileContent :: Text }
+    | AddImport { filePath :: OsPath, fileContent :: Text }
+    | AddAction { filePath :: OsPath, fileContent :: Text }
+    | AddToDataConstructor { dataConstructor :: Text, filePath :: OsPath, fileContent :: Text }
+    | AddMountToFrontController { filePath :: OsPath, applicationName :: Text }
+    | EnsureDirectory { directory :: OsPath }
     | RunShellCommand { shellCommand :: Text }
     deriving (Show, Eq)
 
@@ -19,13 +22,8 @@
 
 fieldsForTable :: [Statement] -> Text -> Maybe [Text]
 fieldsForTable database name =
-    case getTable database name of
-        Just (StatementCreateTable CreateTable { columns, primaryKeyConstraint }) -> columns
-                |> filter (columnRelevantForCreateOrEdit primaryKeyConstraint)
-                |> map (.name)
-                |> map columnNameToFieldName
-                |> Just
-        _ -> Nothing
+    columnsForTable database name
+        |> fmap (map (\col -> col.name |> columnNameToFieldName))
 -- | Returns True when a column should be part of the generated controller or forms
 --
 -- Returrns @False@ for primary keys, or fields such as @created_at@
@@ -43,3 +41,165 @@
         isTable :: Statement -> Bool
         isTable table@(StatementCreateTable CreateTable { name = name' }) | name == name' = True
         isTable _ = False
+
+-- | Like 'fieldsForTable' but returns full 'Column' records (filtered to exclude PKs and auto-timestamps)
+columnsForTable :: [Statement] -> Text -> Maybe [Column]
+columnsForTable database name =
+    case getTable database name of
+        Just (StatementCreateTable CreateTable { columns, primaryKeyConstraint }) -> columns
+                |> filter (columnRelevantForCreateOrEdit primaryKeyConstraint)
+                |> Just
+        _ -> Nothing
+
+-- | Returns @[(columnName, referenceTable)]@ for all foreign key constraints on a table.
+--
+-- Scans both inline table constraints and top-level 'AddConstraint' statements.
+foreignKeysForTable :: [Statement] -> Text -> [(Text, Text)]
+foreignKeysForTable schema tableName = inlineFK <> topLevelFK
+    where
+        inlineFK = case getTable schema tableName of
+            Just (StatementCreateTable CreateTable { constraints }) ->
+                [(c.columnName, c.referenceTable) | c@ForeignKeyConstraint {} <- constraints]
+            _ -> []
+        topLevelFK =
+            [ (c.columnName, c.referenceTable)
+            | AddConstraint { tableName = tbl, constraint = c@ForeignKeyConstraint {} } <- schema
+            , tbl == tableName
+            ]
+
+-- | Returns column names that have single-column UNIQUE constraints (from 'AddConstraint' or inline table constraints).
+--
+-- Does not include primary key columns.
+uniqueColumnsForTable :: [Statement] -> Text -> [Text]
+uniqueColumnsForTable schema tableName = inlineUnique <> topLevelUnique
+    where
+        inlineUnique = case getTable schema tableName of
+            Just (StatementCreateTable CreateTable { constraints }) ->
+                [col | UniqueConstraint { columnNames = [col] } <- constraints]
+            _ -> []
+        topLevelUnique =
+            [ col
+            | AddConstraint { tableName = tbl, constraint = UniqueConstraint { columnNames = [col] } } <- schema
+            , tbl == tableName
+            ]
+
+loadAppSchema :: IO [Statement]
+loadAppSchema = SchemaDesigner.parseSchemaSql >>= \case
+    Left _parserError -> pure []
+    Right statements -> pure statements
+
+-- | Ensures a name has the given suffix, returning both the suffixed and unsuffixed versions.
+--
+-- >>> ensureSuffix "View" "EditView"
+-- ("EditView", "Edit")
+-- >>> ensureSuffix "View" "Edit"
+-- ("EditView", "Edit")
+ensureSuffix :: Text -> Text -> (Text, Text)
+ensureSuffix suffix name
+    | suffix `isSuffixOf` name = (name, Text.dropEnd (Text.length suffix) name)
+    | otherwise = (name <> suffix, name)
+
+-- | Build a qualified Haskell module name from application, category, controller, and module name.
+--
+-- >>> qualifiedModuleName "Web" "View" "Users" "Edit"
+-- "Web.View.Users.Edit"
+qualifiedModuleName :: Text -> Text -> Text -> Text -> Text
+qualifiedModuleName applicationName category controllerName moduleName =
+    applicationName <> "." <> category <> "." <> controllerName <> "." <> moduleName
+
+-- | Configuration for generating standard CRUD action bodies.
+data ActionBodyConfig = ActionBodyConfig
+    { singularName :: Text          -- ^ e.g. "User"
+    , modelVariableSingular :: Text -- ^ e.g. "user"
+    , idFieldName :: Text           -- ^ e.g. "userId"
+    , model :: Text                 -- ^ e.g. "User"
+    , indexAction :: Text           -- ^ redirect target for create/delete, e.g. "UsersAction"
+    , tableFound :: Bool            -- ^ whether the corresponding table exists in the schema
+    }
+
+generateShowActionBody :: ActionBodyConfig -> Text
+generateShowActionBody config
+    | config.tableFound =
+        ""
+        <> "    action Show" <> config.singularName <> "Action { " <> config.idFieldName <> " } = do\n"
+        <> "        " <> config.modelVariableSingular <> " <- fetch " <> config.idFieldName <> "\n"
+        <> "        render ShowView { .. }\n"
+    | otherwise =
+        ""
+        <> "    action Show" <> config.singularName <> "Action = do\n"
+        <> "        render ShowView { .. }\n"
+
+generateNewActionBody :: ActionBodyConfig -> Text
+generateNewActionBody config =
+    ""
+    <> "    action New" <> config.singularName <> "Action = do\n"
+    <> "        let " <> config.modelVariableSingular <> " = newRecord\n"
+    <> "        render NewView { .. }\n"
+
+generateEditActionBody :: ActionBodyConfig -> Text
+generateEditActionBody config
+    | config.tableFound =
+        ""
+        <> "    action Edit" <> config.singularName <> "Action { " <> config.idFieldName <> " } = do\n"
+        <> "        " <> config.modelVariableSingular <> " <- fetch " <> config.idFieldName <> "\n"
+        <> "        render EditView { .. }\n"
+    | otherwise =
+        ""
+        <> "    action Edit" <> config.singularName <> "Action = do\n"
+        <> "        render EditView { .. }\n"
+
+generateUpdateActionBody :: ActionBodyConfig -> Text
+generateUpdateActionBody config
+    | config.tableFound =
+        ""
+        <> "    action Update" <> config.singularName <> "Action { " <> config.idFieldName <> " } = do\n"
+        <> "        " <> config.modelVariableSingular <> " <- fetch " <> config.idFieldName <> "\n"
+        <> "        " <> config.modelVariableSingular <> "\n"
+        <> "            |> build" <> config.singularName <> "\n"
+        <> "            |> ifValid \\case\n"
+        <> "                Left " <> config.modelVariableSingular <> " -> render EditView { .. }\n"
+        <> "                Right " <> config.modelVariableSingular <> " -> do\n"
+        <> "                    " <> config.modelVariableSingular <> " <- " <> config.modelVariableSingular <> " |> updateRecord\n"
+        <> "                    setSuccessMessage \"" <> config.model <> " updated\"\n"
+        <> "                    redirectTo Edit" <> config.singularName <> "Action { .. }\n"
+    | otherwise =
+        ""
+        <> "    action Update" <> config.singularName <> "Action = do\n"
+        <> "        let " <> config.modelVariableSingular <> " = newRecord\n"
+        <> "        " <> config.modelVariableSingular <> "\n"
+        <> "            |> build" <> config.singularName <> "\n"
+        <> "            |> ifValid \\case\n"
+        <> "                Left " <> config.modelVariableSingular <> " -> render EditView { .. }\n"
+        <> "                Right " <> config.modelVariableSingular <> " -> do\n"
+        <> "                    " <> config.modelVariableSingular <> " <- " <> config.modelVariableSingular <> " |> updateRecord\n"
+        <> "                    setSuccessMessage \"" <> config.model <> " updated\"\n"
+        <> "                    redirectTo Edit" <> config.singularName <> "Action\n"
+
+generateCreateActionBody :: ActionBodyConfig -> Text
+generateCreateActionBody config =
+    ""
+    <> "    action Create" <> config.singularName <> "Action = do\n"
+    <> "        let " <> config.modelVariableSingular <> " = newRecord @" <> config.model <> "\n"
+    <> "        " <> config.modelVariableSingular <> "\n"
+    <> "            |> build" <> config.singularName <> "\n"
+    <> "            |> ifValid \\case\n"
+    <> "                Left " <> config.modelVariableSingular <> " -> render NewView { .. } \n"
+    <> "                Right " <> config.modelVariableSingular <> " -> do\n"
+    <> "                    " <> config.modelVariableSingular <> " <- " <> config.modelVariableSingular <> " |> createRecord\n"
+    <> "                    setSuccessMessage \"" <> config.model <> " created\"\n"
+    <> "                    redirectTo " <> config.indexAction <> "\n"
+
+generateDeleteActionBody :: ActionBodyConfig -> Text
+generateDeleteActionBody config
+    | config.tableFound =
+        ""
+        <> "    action Delete" <> config.singularName <> "Action { " <> config.idFieldName <> " } = do\n"
+        <> "        " <> config.modelVariableSingular <> " <- fetch " <> config.idFieldName <> "\n"
+        <> "        deleteRecord " <> config.modelVariableSingular <> "\n"
+        <> "        setSuccessMessage \"" <> config.model <> " deleted\"\n"
+        <> "        redirectTo " <> config.indexAction <> "\n"
+    | otherwise =
+        ""
+        <> "    action Delete" <> config.singularName <> "Action = do\n"
+        <> "        setSuccessMessage \"" <> config.model <> " deleted\"\n"
+        <> "        redirectTo " <> config.indexAction <> "\n"
diff --git a/IHP/IDE/CodeGen/View/NewAction.hs b/IHP/IDE/CodeGen/View/NewAction.hs
--- a/IHP/IDE/CodeGen/View/NewAction.hs
+++ b/IHP/IDE/CodeGen/View/NewAction.hs
@@ -53,7 +53,7 @@
                             name="doGenerateView"
                             id="doGenerateView"
                             />
-                        <label class="pl-1" for="doGenerateView">With View</label>
+                        <label class="ps-1" for="doGenerateView">With View</label>
                     </div>
                 </form>|]
             renderControllerOptions = forM_ controllers (\x -> [hsx|<option>{x}</option>|])
@@ -86,7 +86,7 @@
                             checked = {doGenerateView}
                             onchange = "this.form.submit()"
                             />
-                        <label class="pl-1" for="doGenerateView">With View</label>
+                        <label class="ps-1" for="doGenerateView">With View</label>
                         <input type="hidden" name="name" value={actionName}/>
                         <input type="hidden" name="controllerName" value={controllerName}/>
                     </form>
diff --git a/IHP/IDE/CodeGen/View/NewController.hs b/IHP/IDE/CodeGen/View/NewController.hs
--- a/IHP/IDE/CodeGen/View/NewController.hs
+++ b/IHP/IDE/CodeGen/View/NewController.hs
@@ -11,6 +11,13 @@
     , applicationName :: Text
     , applications :: [Text]
     , pagination :: Bool
+    , indexAction :: Bool
+    , newAction :: Bool
+    , showAction :: Bool
+    , createAction :: Bool
+    , editAction :: Bool
+    , updateAction :: Bool
+    , deleteAction :: Bool
     }
 
 instance View NewControllerView where
@@ -47,6 +54,13 @@
                     <input type="hidden" name="name" value={controllerName}/>
                     <input type="hidden" name="applicationName" value={applicationName}/>
                     <input type="hidden" name="pagination" value={inputValue pagination}/>
+                    <input type="hidden" name="indexAction" value={inputValue indexAction}/>
+                    <input type="hidden" name="newAction" value={inputValue newAction}/>
+                    <input type="hidden" name="showAction" value={inputValue showAction}/>
+                    <input type="hidden" name="createAction" value={inputValue createAction}/>
+                    <input type="hidden" name="editAction" value={inputValue editAction}/>
+                    <input type="hidden" name="updateAction" value={inputValue updateAction}/>
+                    <input type="hidden" name="deleteAction" value={inputValue deleteAction}/>
 
                     <button class="btn btn-primary" type="submit">Generate</button>
                 </form>
@@ -54,24 +68,37 @@
                 <div class="generator-options">
                     <form method="POST" action={NewControllerAction}>
                         <input type="hidden" name="name" value={controllerName}/>
-                        <input type="hidden" name="applicationName" value={applicationName}/>
-                        <input
-                            type="checkbox"
-                            name="pagination"
-                            id="pagination"
-                            checked={pagination}
-                            onchange="this.form.submit()"
-                            />
-                        <label class="pl-1" for="pagination">Pagination</label>
+                        {renderApplicationNameSelector}
+                        <input type="checkbox" name="pagination" id="pagination" checked={pagination} onchange="this.form.submit()"/>
+                        <label class="ps-1" for="pagination">Pagination</label>
+
+                        <span class="ms-3">Actions:</span>
+                        {renderActionCheckbox "indexAction" "Index" indexAction}
+                        {renderActionCheckbox "newAction" "New" newAction}
+                        {renderActionCheckbox "showAction" "Show" showAction}
+                        {renderActionCheckbox "createAction" "Create" createAction}
+                        {renderActionCheckbox "editAction" "Edit" editAction}
+                        {renderActionCheckbox "updateAction" "Update" updateAction}
+                        {renderActionCheckbox "deleteAction" "Delete" deleteAction}
                     </form>
                 </div>
             |]
+            renderActionCheckbox :: Text -> Text -> Bool -> Html
+            renderActionCheckbox name label checked = [hsx|
+                <input type="checkbox" name={name} id={name} checked={checked} onchange="this.form.submit()"/>
+                <input type="hidden" name={name} value={inputValue False}/>
+                <label class="ps-1 me-2" for={name}>{label}</label>
+            |]
+            renderApplicationNameSelector = if length applications /= 1
+                then renderApplicationSelector
+                else [hsx|<input type="hidden" name="applicationName" value={applicationName}/>|]
             renderApplicationOptions = forM_ applications (\x -> [hsx|<option selected={x == applicationName}>{x}</option>|])
             renderApplicationSelector = [hsx|
                 <select
                     name="applicationName"
                     class="form-control select2-simple"
                     size="1"
+                    onchange="this.form.submit()"
                 >
                     {renderApplicationOptions}
                 </select>|]
diff --git a/IHP/IDE/CodeGen/View/NewMigration.hs b/IHP/IDE/CodeGen/View/NewMigration.hs
--- a/IHP/IDE/CodeGen/View/NewMigration.hs
+++ b/IHP/IDE/CodeGen/View/NewMigration.hs
@@ -54,7 +54,7 @@
                         />
                         <input type="hidden" name="runMigration" value={inputValue False}/>
                         <input type="hidden" name="description" value=""/>
-                        <label class="pl-1" for="runMigration">Run Migration after <q>Generate</q></label>
+                        <label class="ps-1" for="runMigration">Run Migration after <q>Generate</q></label>
                     </form>
                 </div>
             |]
diff --git a/IHP/IDE/CodeGen/ViewGenerator.hs b/IHP/IDE/CodeGen/ViewGenerator.hs
--- a/IHP/IDE/CodeGen/ViewGenerator.hs
+++ b/IHP/IDE/CodeGen/ViewGenerator.hs
@@ -1,10 +1,8 @@
-module IHP.IDE.CodeGen.ViewGenerator (buildPlan, buildPlan', ViewConfig (..)) where
+module IHP.IDE.CodeGen.ViewGenerator (buildPlan, buildPlan', ViewConfig (..), postgresTypeToFieldHelper) where
 
 import IHP.Prelude
-import qualified Data.Text as Text
 import IHP.IDE.CodeGen.Types
-import qualified IHP.IDE.SchemaDesigner.Parser as SchemaDesigner
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import Text.Countable (singularize, pluralize)
 
 data ViewConfig = ViewConfig
@@ -20,9 +18,7 @@
     if (null viewName' || null controllerName')
         then pure $ Left "Neither view name nor controller name can be empty"
         else do
-            schema <- SchemaDesigner.parseSchemaSql >>= \case
-                Left parserError -> pure []
-                Right statements -> pure statements
+            schema <- loadAppSchema
             let modelName = tableNameToModelName controllerName'
             let controllerName = tableNameToControllerName controllerName'
             let viewName = tableNameToViewName viewName'
@@ -33,7 +29,7 @@
 -- E.g. qualifiedViewModuleName config "Edit" == "Web.View.Users.Edit"
 qualifiedViewModuleName :: ViewConfig -> Text -> Text
 qualifiedViewModuleName config viewName =
-    config.applicationName <> ".View." <> config.controllerName <> "." <> viewName
+    qualifiedModuleName config.applicationName "View" config.controllerName viewName
 
 buildPlan' :: [Statement] -> ViewConfig -> [GeneratorAction]
 buildPlan' schema config =
@@ -44,12 +40,7 @@
             pluralName = singularName |> lcfirst |> pluralize |> ucfirst -- TODO: `pluralize` Should Support Lower-Cased Words
             singularVariableName = lcfirst singularName
             pluralVariableName = lcfirst controllerName
-            nameWithSuffix = if "View" `isSuffixOf` name
-                then name
-                else name <> "View" --e.g. "Test" -> "TestView"
-            nameWithoutSuffix = if "View" `isSuffixOf` name
-                then Text.replace "View" "" name
-                else name --e.g. "TestView" -> "Test"
+            (nameWithSuffix, nameWithoutSuffix) = ensureSuffix "View" name
 
             indexAction = pluralName <> "Action"
             specialCases = [
@@ -61,12 +52,23 @@
 
             paginationEnabled = config.paginationEnabled
 
-            modelFields :: [Text]
-            modelFields =  [ modelNameToTableName pluralVariableName, pluralVariableName ]
-                    |> mapMaybe (fieldsForTable schema)
+            tableFound :: Bool
+            tableFound = [ modelNameToTableName pluralVariableName, pluralVariableName ]
+                    |> mapMaybe (columnsForTable schema)
+                    |> headMay
+                    |> isJust
+
+            modelColumns :: [Column]
+            modelColumns = [ modelNameToTableName pluralVariableName, pluralVariableName ]
+                    |> mapMaybe (columnsForTable schema)
                     |> head
                     |> fromMaybe []
 
+            foreignKeySet :: [(Text, Text)]
+            foreignKeySet = [ modelNameToTableName pluralVariableName, pluralVariableName ]
+                    |> map (foreignKeysForTable schema)
+                    |> concat
+
             -- when using the trimming quasiquoter we can't have another |] closure, like for the one we use with hsx.
             qqClose = "|]"
 
@@ -99,6 +101,16 @@
                     pluralizedName = pluralize name
 
 
+            showViewBody =
+                if null modelColumns
+                then "<p>{" <> singularVariableName <> "}</p>"
+                else "<dl>" <> mconcat (map showColumn modelColumns) <> "\n</dl>"
+                where
+                    showColumn column =
+                        let fieldName = columnNameToFieldName column.name
+                            label = columnNameToFieldLabel column.name
+                        in "\n    <dt>" <> label <> "</dt><dd>{" <> singularVariableName <> "." <> fieldName <> "}</dd>"
+
             showView = [trimming|
                 ${viewHeader}
 
@@ -108,7 +120,7 @@
                     html ShowView { .. } = [hsx|
                         {breadcrumb}
                         <h1>Show ${singularName}</h1>
-                        <p>{${singularVariableName}}</p>
+                        ${showViewBody}
 
                     ${qqClose}
                         where
@@ -129,7 +141,14 @@
             |]
                 where
                     formFields =
-                        intercalate "\n" (map (\field -> "{(textField #" <> field <> ")}") modelFields)
+                        intercalate "\n" (map columnToFormField modelColumns)
+                    columnToFormField column =
+                        let fieldName = columnNameToFieldName column.name
+                            isForeignKey = any (\(colName, _) -> colName == column.name) foreignKeySet
+                            helper = postgresTypeToFieldHelper column.columnType
+                        in if isForeignKey
+                            then "{- " <> fieldName <> " needs to be a selectField -}\n    {(" <> helper <> " #" <> fieldName <> ")}"
+                            else "{(" <> helper <> " #" <> fieldName <> ")}"
 
 
             newView = [trimming|
@@ -172,6 +191,16 @@
                 ${renderForm}
             |]
 
+            indexHeaders =
+                if null modelColumns
+                then "<th>" <> singularName <> "</th>"
+                else intercalate "\n" (map (\c -> "<th>" <> columnNameToFieldLabel c.name <> "</th>") modelColumns)
+
+            indexCells =
+                if null modelColumns
+                then "<td>{" <> singularVariableName <> "}</td>"
+                else intercalate "\n" (map (\c -> "<td>{" <> singularVariableName <> "." <> columnNameToFieldName c.name <> "}</td>") modelColumns)
+
             indexView = [trimming|
                 ${viewHeader}
 
@@ -186,7 +215,7 @@
                             <table class="table">
                                 <thead>
                                     <tr>
-                                        <th>${singularName}</th>
+                                        ${indexHeaders}
                                         <th></th>
                                         <th></th>
                                         <th></th>
@@ -205,22 +234,43 @@
                 render${singularName} :: ${singularName} -> Html
                 render${singularName} ${singularVariableName} = [hsx|
                     <tr>
-                        <td>{${singularVariableName}}</td>
-                        <td><a href={Show${singularName}Action ${singularVariableName}.id}>Show</a></td>
-                        <td><a href={Edit${singularName}Action ${singularVariableName}.id} class="text-muted">Edit</a></td>
-                        <td><a href={Delete${singularName}Action ${singularVariableName}.id} class="js-delete text-muted">Delete</a></td>
+                        ${indexCells}
+                        <td><a href={${showLink}}>Show</a></td>
+                        <td><a href={${editLink}} class="text-muted">Edit</a></td>
+                        <td><a href={${deleteLink}} class="js-delete text-muted">Delete</a></td>
                     </tr>
                 ${qqClose}
             |]
                 where
                     importPagination = if paginationEnabled then ", pagination :: Pagination" else ""
                     renderPagination = if paginationEnabled then "{renderPagination pagination}" else ""
+                    idSuffix = if tableFound then " " <> singularVariableName <> ".id" else ""
+                    showLink = "Show" <> singularName <> "Action" <> idSuffix
+                    editLink = "Edit" <> singularName <> "Action" <> idSuffix
+                    deleteLink = "Delete" <> singularName <> "Action" <> idSuffix
 
 
 
             chosenView = fromMaybe genericView (lookup nameWithSuffix specialCases)
         in
-            [ EnsureDirectory { directory = config.applicationName <> "/View/" <> controllerName }
-            , CreateFile { filePath = config.applicationName <> "/View/" <> controllerName <> "/" <> nameWithoutSuffix <> ".hs", fileContent = chosenView }
-            , AddImport { filePath = config.applicationName <> "/Controller/" <> controllerName <> ".hs", fileContent = "import " <> qualifiedViewModuleName config nameWithoutSuffix }
+            [ EnsureDirectory { directory = textToOsPath (config.applicationName <> "/View/" <> controllerName) }
+            , CreateFile { filePath = textToOsPath (config.applicationName <> "/View/" <> controllerName <> "/" <> nameWithoutSuffix <> ".hs"), fileContent = chosenView }
+            , AddImport { filePath = textToOsPath (config.applicationName <> "/Controller/" <> controllerName <> ".hs"), fileContent = "import " <> qualifiedViewModuleName config nameWithoutSuffix }
             ]
+
+-- | Maps a Postgres column type to the appropriate IHP form field helper name.
+postgresTypeToFieldHelper :: PostgresType -> Text
+postgresTypeToFieldHelper PBoolean = "checkboxField"
+postgresTypeToFieldHelper PInt = "numberField"
+postgresTypeToFieldHelper PSmallInt = "numberField"
+postgresTypeToFieldHelper PBigInt = "numberField"
+postgresTypeToFieldHelper PSerial = "numberField"
+postgresTypeToFieldHelper PBigserial = "numberField"
+postgresTypeToFieldHelper PReal = "numberField"
+postgresTypeToFieldHelper PDouble = "numberField"
+postgresTypeToFieldHelper (PNumeric _ _) = "numberField"
+postgresTypeToFieldHelper PDate = "dateField"
+postgresTypeToFieldHelper PTimestamp = "dateTimeField"
+postgresTypeToFieldHelper PTimestampWithTimezone = "dateTimeField"
+postgresTypeToFieldHelper PTime = "timeField"
+postgresTypeToFieldHelper _ = "textField"
diff --git a/IHP/IDE/Data/Controller.hs b/IHP/IDE/Data/Controller.hs
--- a/IHP/IDE/Data/Controller.hs
+++ b/IHP/IDE/Data/Controller.hs
@@ -10,19 +10,23 @@
 import IHP.IDE.Data.View.EditValue
 import IHP.IDE.Data.View.ShowForeignKeyHoverCard
 
-import qualified Database.PostgreSQL.Simple as PG
-import qualified Database.PostgreSQL.Simple.FromField as PG
-import qualified Database.PostgreSQL.Simple.FromRow as PG
-import qualified Database.PostgreSQL.Simple.ToField as PG
-import qualified Database.PostgreSQL.Simple.Types as PG
+import qualified Hasql.Session as Session
+import qualified Hasql.Errors as HasqlErrors
+import qualified Hasql.Decoders as Decoders
+import qualified Hasql.DynamicStatements.Snippet as Snippet
+import Hasql.DynamicStatements.Snippet (Snippet)
+import qualified Hasql.Pool as HasqlPool
+import IHP.Hasql.Pool (usePoolWithRetry)
 import qualified Data.Text as T
-import qualified Data.ByteString.Builder
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.KeyMap as Aeson
+import qualified Data.Aeson.Key as Aeson
+import qualified Data.List as List
+import IHP.QueryBuilder.HasqlHelpers (wrapDynamicQuery, quoteIdentifier)
 
 instance Controller DataController where
     action ShowDatabaseAction = do
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
-        PG.close connection
+        tableNames <- fetchTableNames
         case headMay tableNames of
             Just tableName -> jumpToAction ShowTableRowsAction { tableName }
             Nothing -> render ShowDatabaseView { .. }
@@ -30,13 +34,11 @@
     action ShowTableRowsAction { tableName } = do
         let page :: Int = paramOrDefault @Int 1 "page"
         let pageSize :: Int = paramOrDefault @Int 20 "rows"
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
-        primaryKeyFields <- tablePrimaryKeyFields connection tableName
-        rows :: [[DynamicField]] <- fetchRowsPage connection tableName page pageSize
-        tableCols <- fetchTableCols connection tableName
-        totalRows <- tableLength connection tableName
-        PG.close connection
+        tableNames <- fetchTableNames
+        primaryKeyFields <- tablePrimaryKeyFields tableName
+        tableCols <- fetchTableCols tableName
+        rows <- map (reorderFields tableCols) <$> fetchRowsPage tableName page pageSize
+        totalRows <- tableLength tableName
         render ShowTableRowsView { .. }
 
     action NewQueryAction = do
@@ -45,260 +47,300 @@
         render ShowQueryView { .. }
 
     action QueryAction = do
-        connection <- connectToAppDb
         let queryText = param @Text "query"
         when (isEmpty queryText) do
             redirectTo NewQueryAction
 
-        let query = fromString $ cs queryText
-
-        queryResult :: Maybe (Either PG.SqlError SqlConsoleResult) <- Just <$> if isQuery queryText then
-                (Right . SelectQueryResult <$> PG.query_ connection query) `catch` (pure . Left)
-            else
-                (Right . InsertOrUpdateResult <$> PG.execute_ connection query) `catch` (pure . Left)
+        queryResult :: Maybe (Either SqlConsoleError SqlConsoleResult) <- do
+            let pool = ?modelContext.hasqlPool
+            Just <$> if isQuery queryText then do
+                    let snippet = wrapDynamicQuery (Snippet.sql (cs queryText))
+                    let statement = Snippet.toStatement snippet dynamicFieldDecoder
+                    let session = Session.statement () statement
+                    result <- HasqlPool.use pool session
+                    case result of
+                        Right rows -> pure (Right (SelectQueryResult rows))
+                        Left err -> pure (Left (usageErrorToConsoleError err))
+                else do
+                    let statement = Snippet.toStatement (Snippet.sql (cs queryText)) Decoders.rowsAffected
+                    let session = Session.statement () statement
+                    result <- HasqlPool.use pool session
+                    case result of
+                        Right count -> pure (Right (InsertOrUpdateResult count))
+                        Left err -> pure (Left (usageErrorToConsoleError err))
 
-        PG.close connection
         render ShowQueryView { .. }
 
     action DeleteEntryAction { primaryKey, tableName } = do
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
-        primaryKeyFields <- tablePrimaryKeyFields connection tableName
+        primaryKeyFields <- tablePrimaryKeyFields tableName
         let primaryKeyValues = T.splitOn "---" primaryKey
-        let query = "DELETE FROM " <> tableName <> " WHERE " <> intercalate " AND " ((<> " = ?") <$> primaryKeyFields)
-        PG.execute connection (PG.Query . cs $! query) primaryKeyValues
-        PG.close connection
+        let whereClause = mconcat $ List.intersperse (Snippet.sql " AND ") $
+                zipWith (\field val -> quoteIdentifier field <> Snippet.sql "::text = " <> Snippet.param val) primaryKeyFields primaryKeyValues
+        let snippet = Snippet.sql "DELETE FROM " <> quoteIdentifier tableName <> Snippet.sql " WHERE " <> whereClause
+        runSnippetExec snippet
         redirectTo ShowTableRowsAction { .. }
 
     action NewRowAction { tableName } = do
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
-
-        rows :: [[DynamicField]] <- fetchRows connection tableName
-
-        tableCols <- fetchTableCols connection tableName
-
-        PG.close connection
+        tableNames <- fetchTableNames
+        tableCols <- fetchTableCols tableName
+        rows :: [[DynamicField]] <- map (reorderFields tableCols) <$> fetchRows tableName
         render NewRowView { .. }
 
     action CreateRowAction = do
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
         let tableName = param "tableName"
-        tableCols <- fetchTableCols connection tableName
-        let values :: [PG.Action] = map (\col -> parseValues (param @Bool (cs (col.columnName) <> "_")) (param @Bool (cs (col.columnName) <> "-isBoolean")) (param @Text (cs (col.columnName)))) tableCols
-        let query = "INSERT INTO " <> tableName <> " VALUES (" <> intercalate "," (map (const "?") values) <> ")"
-        PG.execute connection (PG.Query . cs $! query) values
-        PG.close connection
+        tableCols <- fetchTableCols tableName
+        let values :: [Snippet] = map (\col -> parseValues (param @Bool (cs (col.columnName) <> "_")) (param @Bool (cs (col.columnName) <> "-isBoolean")) (param @Text (cs (col.columnName)))) tableCols
+        let snippet = Snippet.sql "INSERT INTO " <> quoteIdentifier tableName <> Snippet.sql " VALUES (" <> (mconcat $ List.intersperse (Snippet.sql ", ") values) <> Snippet.sql ")"
+        runSnippetExec snippet
         redirectTo ShowTableRowsAction { .. }
 
     action EditRowAction { tableName, targetPrimaryKey } = do
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
-        primaryKeyFields <- tablePrimaryKeyFields connection tableName
-
-        rows :: [[DynamicField]] <- fetchRows connection tableName
-
-        tableCols <- fetchTableCols connection tableName
+        tableNames <- fetchTableNames
+        primaryKeyFields <- tablePrimaryKeyFields tableName
+        tableCols <- fetchTableCols tableName
+        rows :: [[DynamicField]] <- map (reorderFields tableCols) <$> fetchRows tableName
         let targetPrimaryKeyValues = T.splitOn "---" targetPrimaryKey
-        values <- fetchRow connection (cs tableName) targetPrimaryKeyValues
-        let (Just rowValues) = head values
-        PG.close connection
+        values <- fetchRow tableName targetPrimaryKeyValues
+        rowValues <- case values of
+            [rowValues] -> pure (reorderFields tableCols rowValues)
+            _ -> error ("Row not found in " <> cs tableName)
         render EditRowView { .. }
 
     action UpdateRowAction = do
         let tableName = param "tableName"
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
-        tableCols <- fetchTableCols connection tableName
-        primaryKeyFields <- tablePrimaryKeyFields connection tableName
+        tableCols <- fetchTableCols tableName
+        primaryKeyFields <- tablePrimaryKeyFields tableName
 
-        let values :: [PG.Action] = map (\col -> parseValues (param @Bool (cs (col.columnName) <> "_")) (param @Bool (cs (col.columnName) <> "-isBoolean")) (param @Text (cs (col.columnName)))) tableCols
-        let columns :: [Text] = map (\col -> cs (col.columnName)) tableCols
-        let primaryKeyValues = map (\pkey -> "'" <> (param @Text (cs pkey <> "-pk")) <> "'") primaryKeyFields
+        let values :: [Snippet] = map (\col -> parseValues (param @Bool (cs (col.columnName) <> "_")) (param @Bool (cs (col.columnName) <> "-isBoolean")) (param @Text (cs (col.columnName)))) tableCols
+        let columns :: [Text] = map (\col -> col.columnName) tableCols
 
-        let query = "UPDATE " <> tableName <> " SET " <> intercalate ", " (updateValues (zip columns (map (const "?") values))) <> " WHERE " <> intercalate " AND " (updateValues (zip primaryKeyFields primaryKeyValues))
-        PG.execute connection (PG.Query . cs $! query) values
-        PG.close connection
+        let setClause = mconcat $ List.intersperse (Snippet.sql ", ") $
+                zipWith (\col val -> quoteIdentifier col <> Snippet.sql " = " <> val) columns values
+        let whereClause = mconcat $ List.intersperse (Snippet.sql " AND ") $
+                map (\pkey -> quoteIdentifier pkey <> Snippet.sql "::text = " <> Snippet.param (param @Text (cs pkey <> "-pk"))) primaryKeyFields
+
+        let snippet = Snippet.sql "UPDATE " <> quoteIdentifier tableName <> Snippet.sql " SET " <> setClause <> Snippet.sql " WHERE " <> whereClause
+        runSnippetExec snippet
         redirectTo ShowTableRowsAction { .. }
 
     action EditRowValueAction { tableName, targetName, id } = do
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
-
-        rows :: [[DynamicField]] <- fetchRows connection tableName
-
+        tableNames <- fetchTableNames
+        tableCols <- fetchTableCols tableName
+        rows :: [[DynamicField]] <- map (reorderFields tableCols) <$> fetchRows tableName
         let targetId = cs id
-        PG.close connection
         render EditValueView { .. }
 
     action ToggleBooleanFieldAction { tableName, targetName, targetPrimaryKey } = do
         let id :: String = cs (param @Text "id")
         let tableName = param "tableName"
-        connection <- connectToAppDb
-        tableNames <- fetchTableNames connection
-        tableCols <- fetchTableCols connection tableName
-        primaryKeyFields <- tablePrimaryKeyFields connection tableName
-        let targetPrimaryKeyValues = PG.Escape . cs <$> T.splitOn "---" targetPrimaryKey
-        let query = PG.Query . cs $! "UPDATE ? SET ? = NOT ? WHERE " <> intercalate " AND " ((<> " = ?") <$> primaryKeyFields)
-        let params = [PG.toField $ PG.Identifier tableName, PG.toField $ PG.Identifier targetName, PG.toField $ PG.Identifier targetName] <> targetPrimaryKeyValues
-        PG.execute connection query params
-        PG.close connection
+        primaryKeyFields <- tablePrimaryKeyFields tableName
+        let targetPrimaryKeyValues = T.splitOn "---" targetPrimaryKey
+        let whereClause = mconcat $ List.intersperse (Snippet.sql " AND ") $
+                zipWith (\field val -> quoteIdentifier field <> Snippet.sql "::text = " <> Snippet.param val) primaryKeyFields targetPrimaryKeyValues
+        let snippet = Snippet.sql "UPDATE " <> quoteIdentifier tableName <> Snippet.sql " SET " <> quoteIdentifier targetName <> Snippet.sql " = NOT " <> quoteIdentifier targetName <> Snippet.sql " WHERE " <> whereClause
+        runSnippetExec snippet
         redirectTo ShowTableRowsAction { .. }
 
     action UpdateValueAction = do
         let id :: String = cs (param @Text "id")
         let tableName = param "tableName"
-        connection <- connectToAppDb
-        let targetCol = param "targetName"
-        let targetValue = param "targetValue"
-        let query = "UPDATE " <> tableName <> " SET " <> targetCol <> " = '" <> targetValue <> "' WHERE id = '" <> cs id <> "'"
-        PG.execute_ connection (PG.Query . cs $! query)
-        PG.close connection
+        let targetCol = param @Text "targetName"
+        let targetValue = param @Text "targetValue"
+        let snippet = Snippet.sql "UPDATE " <> quoteIdentifier tableName <> Snippet.sql " SET " <> quoteIdentifier targetCol <> Snippet.sql " = " <> Snippet.param targetValue <> Snippet.sql " WHERE " <> quoteIdentifier "id" <> Snippet.sql "::text = " <> Snippet.param (cs id :: Text)
+        runSnippetExec snippet
         redirectTo ShowTableRowsAction { .. }
 
     action DeleteTableRowsAction { tableName } = do
-        connection <- connectToAppDb
-        let query = "TRUNCATE TABLE " <> tableName
-        PG.execute_ connection (PG.Query . cs $! query)
-        PG.close connection
+        let snippet = Snippet.sql "TRUNCATE TABLE " <> quoteIdentifier tableName
+        runSnippetExec snippet
         redirectTo ShowTableRowsAction { .. }
 
     action AutocompleteForeignKeyColumnAction { tableName, columnName, term } = do
-        connection <- connectToAppDb
-        rows :: Maybe [[DynamicField]] <- do
-            foreignKeyInfo <- fetchForeignKeyInfo connection tableName columnName
-
-            case foreignKeyInfo of
-                Just (foreignTable, foreignColumn) -> Just <$> fetchRowsPage connection foreignTable 1 50
-                Nothing -> pure Nothing
+        foreignKeyInfo <- fetchForeignKeyInfo tableName columnName
 
-        PG.close connection
+        rows :: Maybe [[DynamicField]] <- case foreignKeyInfo of
+            Just (foreignTable, foreignColumn) -> Just <$> fetchRowsPage foreignTable 1 50
+            Nothing -> pure Nothing
 
         case rows of
             Just rows -> renderJson rows
             Nothing -> renderNotFound
 
     action ShowForeignKeyHoverCardAction { tableName, id, columnName } = do
-        connection <- connectToAppDb
         hovercardData <- do
-            [Only (foreignId :: UUID)] <- PG.query connection "SELECT ? FROM ? WHERE id = ?" (PG.Identifier columnName, PG.Identifier tableName, id)
+            let fetchIdSnippet = Snippet.sql "SELECT " <> quoteIdentifier columnName <> Snippet.sql "::text FROM " <> quoteIdentifier tableName <> Snippet.sql " WHERE " <> quoteIdentifier "id" <> Snippet.sql "::text = " <> Snippet.param id
+            foreignIdResult <- runSnippetQuery fetchIdSnippet (Decoders.rowList (Decoders.column (Decoders.nonNullable Decoders.text)))
 
-            foreignKeyInfo <- fetchForeignKeyInfo connection tableName columnName
+            case foreignIdResult of
+                [foreignId] -> do
+                    foreignKeyInfo <- fetchForeignKeyInfo tableName columnName
 
-            case foreignKeyInfo of
-                Just (foreignTable, foreignColumn) -> do
-                    [record] <- PG.query connection "SELECT * FROM ? WHERE ? = ? LIMIT 1" (PG.Identifier foreignTable, PG.Identifier foreignColumn, foreignId)
-                    pure $ Just (record, foreignTable)
-                Nothing -> pure Nothing
-        PG.close connection
+                    case foreignKeyInfo of
+                        Just (foreignTable, foreignColumn) -> do
+                            let fetchRecordSnippet = wrapDynamicQuery (Snippet.sql "SELECT * FROM " <> quoteIdentifier foreignTable <> Snippet.sql " WHERE " <> quoteIdentifier foreignColumn <> Snippet.sql "::text = " <> Snippet.param foreignId <> Snippet.sql " LIMIT 1")
+                            records <- runSnippetQuery fetchRecordSnippet dynamicFieldDecoder
+                            case records of
+                                [record] -> pure $ Just (record, foreignTable)
+                                _ -> pure Nothing
+                        Nothing -> pure Nothing
+                _ -> pure Nothing
 
         case hovercardData of
             Just (record, foreignTableName) -> render ShowForeignKeyHoverCardView { record, foreignTableName }
             Nothing -> renderNotFound
 
-connectToAppDb :: (?context :: ControllerContext) => IO PG.Connection
-connectToAppDb = PG.connectPostgreSQL ?context.frameworkConfig.databaseUrl
+runSnippetQuery :: (?modelContext :: ModelContext) => Snippet -> Decoders.Result a -> IO a
+runSnippetQuery snippet decoder = do
+    let pool = ?modelContext.hasqlPool
+    let statement = Snippet.toPreparableStatement snippet decoder
+    let session = Session.statement () statement
+    usePoolWithRetry pool session
 
-fetchTableNames :: PG.Connection -> IO [Text]
-fetchTableNames connection = do
-    values :: [[Text]] <- PG.query_ connection "SELECT tablename FROM pg_catalog.pg_tables where schemaname = 'public'"
-    pure (join values)
+runSnippetExec :: (?modelContext :: ModelContext) => Snippet -> IO ()
+runSnippetExec snippet = do
+    let pool = ?modelContext.hasqlPool
+    let statement = Snippet.toPreparableStatement snippet Decoders.noResult
+    let session = Session.statement () statement
+    usePoolWithRetry pool session
 
-fetchTableCols :: PG.Connection -> Text -> IO [ColumnDefinition]
-fetchTableCols connection tableName = do
-    PG.query connection "SELECT column_name,data_type,column_default,CASE WHEN is_nullable='YES' THEN true ELSE false END FROM information_schema.columns where table_name = ? ORDER BY ordinal_position" (PG.Only tableName)
+fetchTableNames :: (?modelContext :: ModelContext) => IO [Text]
+fetchTableNames =
+    runSnippetQuery
+        (Snippet.sql "SELECT tablename::text FROM pg_catalog.pg_tables WHERE schemaname = 'public'")
+        (Decoders.rowList (Decoders.column (Decoders.nonNullable Decoders.text)))
 
-fetchRow :: PG.Connection -> Text -> [Text] -> IO [[DynamicField]]
-fetchRow connection tableName primaryKeyValues = do
-    pkFields <- tablePrimaryKeyFields connection tableName
-    let query = "SELECT * FROM " <> tableName <> " WHERE " <> intercalate " AND " ((<> " = ?") <$> pkFields)
-    PG.query connection (PG.Query . cs $! query) primaryKeyValues
+fetchTableCols :: (?modelContext :: ModelContext) => Text -> IO [ColumnDefinition]
+fetchTableCols tableName =
+    runSnippetQuery
+        (Snippet.sql "SELECT column_name::text, data_type::text, column_default::text, CASE WHEN is_nullable='YES' THEN true ELSE false END FROM information_schema.columns WHERE table_name = " <> Snippet.param tableName <> Snippet.sql " ORDER BY ordinal_position")
+        columnDefinitionDecoder
 
-instance PG.FromField DynamicField where
-    fromField field fieldValue = pure DynamicField { .. }
-        where
-            fieldName = fromMaybe "" (PG.name field)
+fetchRow :: (?modelContext :: ModelContext) => Text -> [Text] -> IO [[DynamicField]]
+fetchRow tableName primaryKeyValues = do
+    pkFields <- tablePrimaryKeyFields tableName
+    let whereClause = mconcat $ List.intersperse (Snippet.sql " AND ") $
+            zipWith (\field val -> quoteIdentifier field <> Snippet.sql "::text = " <> Snippet.param val) pkFields primaryKeyValues
+    let snippet = wrapDynamicQuery (Snippet.sql "SELECT * FROM " <> quoteIdentifier tableName <> Snippet.sql " WHERE " <> whereClause)
+    runSnippetQuery snippet dynamicFieldDecoder
 
-instance PG.FromRow ColumnDefinition where
-    fromRow = ColumnDefinition <$> PG.field <*> PG.field <*> PG.field <*> PG.field
+columnDefinitionDecoder :: Decoders.Result [ColumnDefinition]
+columnDefinitionDecoder = Decoders.rowList $
+    ColumnDefinition
+        <$> Decoders.column (Decoders.nonNullable Decoders.text)
+        <*> Decoders.column (Decoders.nonNullable Decoders.text)
+        <*> Decoders.column (Decoders.nullable Decoders.text)
+        <*> Decoders.column (Decoders.nonNullable Decoders.bool)
 
-tablePrimaryKeyFields :: PG.Connection -> Text -> IO [Text]
-tablePrimaryKeyFields connection tableName = do
-    fields <- PG.query connection "SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = ?::regclass AND i.indisprimary" (PG.Only tableName) :: IO [PG.Only Text]
-    pure $ PG.fromOnly <$> fields
+-- | Decoder for dynamic query results wrapped with 'wrapDynamicQuery'.
+-- Each row comes back as a single JSONB column (from row_to_json), which is then
+-- parsed into a list of DynamicField values.
+dynamicFieldDecoder :: Decoders.Result [[DynamicField]]
+dynamicFieldDecoder = Decoders.rowList $
+    Decoders.column (Decoders.nonNullable Decoders.jsonb)
+    |> fmap (\case
+        Aeson.Object obj -> map jsonFieldToDynamicField (Aeson.toList obj)
+        _ -> error "Expected JSON object from row_to_json"
+    )
 
-fetchRows :: FromRow r => PG.Connection -> Text -> IO [r]
-fetchRows connection tableName = do
-    pkFields <- tablePrimaryKeyFields connection tableName
+jsonFieldToDynamicField :: (Aeson.Key, Aeson.Value) -> DynamicField
+jsonFieldToDynamicField (key, val) = DynamicField
+    { fieldValue = case val of
+        Aeson.Null -> Nothing
+        Aeson.String t -> Just (cs t)
+        Aeson.Bool True -> Just "true"
+        Aeson.Bool False -> Just "false"
+        other -> Just (cs (Aeson.encode other))
+    , fieldName = cs (Aeson.toText key)
+    }
 
-    let query = "SELECT * FROM "
-            <> tableName
-            <> (if null pkFields
-                    then ""
-                    else " ORDER BY " <> intercalate ", " pkFields
-                )
+tablePrimaryKeyFields :: (?modelContext :: ModelContext) => Text -> IO [Text]
+tablePrimaryKeyFields tableName =
+    runSnippetQuery
+        (Snippet.sql "SELECT a.attname::text FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = " <> Snippet.param tableName <> Snippet.sql "::regclass AND i.indisprimary")
+        (Decoders.rowList (Decoders.column (Decoders.nonNullable Decoders.text)))
 
-    PG.query_ connection (PG.Query . cs $! query)
+fetchRows :: (?modelContext :: ModelContext) => Text -> IO [[DynamicField]]
+fetchRows tableName = do
+    pkFields <- tablePrimaryKeyFields tableName
 
-fetchRowsPage :: FromRow r => PG.Connection -> Text -> Int -> Int -> IO [r]
-fetchRowsPage connection tableName page rows = do
-    pkFields <- tablePrimaryKeyFields connection tableName
-    let slice = " OFFSET " <> show (page * rows - rows) <> " ROWS FETCH FIRST " <> show rows <> " ROWS ONLY"
-    let query = "SELECT * FROM "
-            <> tableName
-            <> (if null pkFields
-                    then ""
-                    else " ORDER BY " <> intercalate ", " pkFields
-                )
-            <> slice
+    let orderBy = if null pkFields
+            then mempty
+            else Snippet.sql " ORDER BY " <> (mconcat $ List.intersperse (Snippet.sql ", ") (map quoteIdentifier pkFields))
 
-    PG.query_ connection (PG.Query . cs $! query)
+    let snippet = wrapDynamicQuery (Snippet.sql "SELECT * FROM " <> quoteIdentifier tableName <> orderBy)
+    runSnippetQuery snippet dynamicFieldDecoder
 
-tableLength :: PG.Connection -> Text -> IO Int
-tableLength connection tableName = do
-    [Only count] <- PG.query connection "SELECT COUNT(*) FROM ?" [PG.Identifier tableName]
-    pure count
+fetchRowsPage :: (?modelContext :: ModelContext) => Text -> Int -> Int -> IO [[DynamicField]]
+fetchRowsPage tableName page rows = do
+    pkFields <- tablePrimaryKeyFields tableName
 
+    let orderBy = if null pkFields
+            then mempty
+            else Snippet.sql " ORDER BY " <> (mconcat $ List.intersperse (Snippet.sql ", ") (map quoteIdentifier pkFields))
 
--- parseValues sqlMode isBoolField input
-parseValues :: Bool -> Bool -> Text -> PG.Action
-parseValues _ True "on" = PG.toField True
-parseValues _ True "off" = PG.toField False
-parseValues False _ text = PG.toField text
-parseValues _ _ text = PG.Plain (Data.ByteString.Builder.byteString (cs text))
+    let snippet = wrapDynamicQuery (
+            Snippet.sql "SELECT * FROM " <> quoteIdentifier tableName
+            <> orderBy
+            <> Snippet.sql " OFFSET " <> Snippet.param (fromIntegral (page * rows - rows) :: Int64)
+            <> Snippet.sql " ROWS FETCH FIRST " <> Snippet.param (fromIntegral rows :: Int64)
+            <> Snippet.sql " ROWS ONLY"
+            )
+    runSnippetQuery snippet dynamicFieldDecoder
 
-updateValues list = map (\elem -> fst elem <> " = " <> snd elem) list
+tableLength :: (?modelContext :: ModelContext) => Text -> IO Int
+tableLength tableName = do
+    count <- runSnippetQuery
+        (Snippet.sql "SELECT COUNT(*) FROM " <> quoteIdentifier tableName)
+        (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int8)))
+    pure (fromIntegral count)
 
+-- parseValues sqlMode isBoolField input
+parseValues :: Bool -> Bool -> Text -> Snippet
+parseValues _ True "on" = Snippet.param True
+parseValues _ True "off" = Snippet.param False
+parseValues False _ text = Snippet.param text
+parseValues _ _ text = Snippet.sql (cs text)  -- raw SQL mode (for expressions like now(), DEFAULT, NULL)
+
+isQuery :: Text -> Bool
 isQuery sql = T.isInfixOf "SELECT" u
     where u = T.toUpper sql
 
-
-
-fetchForeignKeyInfo :: PG.Connection -> Text -> Text -> IO (Maybe (Text, Text))
-fetchForeignKeyInfo connection tableName columnName = do
-    let sql = [plain|
-        SELECT
-            ccu.table_name AS foreign_table_name,
-            ccu.column_name AS foreign_column_name
-        FROM
-            information_schema.table_constraints AS tc
-            JOIN information_schema.key_column_usage AS kcu
-              ON tc.constraint_name = kcu.constraint_name
-              AND tc.table_schema = kcu.table_schema
-            JOIN information_schema.constraint_column_usage AS ccu
-              ON ccu.constraint_name = tc.constraint_name
-              AND ccu.table_schema = tc.table_schema
-        WHERE
-            tc.constraint_type = 'FOREIGN KEY'
-            AND tc.table_name = ?
-            AND kcu.column_name = ?
-    |]
-    let args = (tableName, columnName)
-    result <- PG.query connection (PG.Query $ cs sql) args
+fetchForeignKeyInfo :: (?modelContext :: ModelContext) => Text -> Text -> IO (Maybe (Text, Text))
+fetchForeignKeyInfo tableName columnName = do
+    let snippet =
+            Snippet.sql "SELECT ccu.table_name::text AS foreign_table_name, ccu.column_name::text AS foreign_column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = "
+            <> Snippet.param tableName
+            <> Snippet.sql " AND kcu.column_name = "
+            <> Snippet.param columnName
+    let decoder = Decoders.rowList $
+            (,) <$> Decoders.column (Decoders.nonNullable Decoders.text)
+                <*> Decoders.column (Decoders.nonNullable Decoders.text)
+    result <- runSnippetQuery snippet decoder
     case result of
         [(foreignTableName, foreignColumnName)] -> pure $ Just (foreignTableName, foreignColumnName)
-        otherwise -> pure $ Nothing
+        _ -> pure Nothing
+
+usageErrorToConsoleError :: HasqlPool.UsageError -> SqlConsoleError
+usageErrorToConsoleError (HasqlPool.SessionUsageError sessionError) = sessionErrorToConsoleError sessionError
+usageErrorToConsoleError err =
+    SqlConsoleError { errorMessage = cs (show err), errorDetail = "", errorHint = "", errorState = "" }
+
+sessionErrorToConsoleError :: HasqlErrors.SessionError -> SqlConsoleError
+sessionErrorToConsoleError (HasqlErrors.StatementSessionError _ _ _ _ _ (HasqlErrors.ServerStatementError (HasqlErrors.ServerError code message detail hint _))) =
+    SqlConsoleError { errorMessage = message, errorDetail = fromMaybe "" detail, errorHint = fromMaybe "" hint, errorState = code }
+sessionErrorToConsoleError (HasqlErrors.ScriptSessionError _ (HasqlErrors.ServerError code message detail hint _)) =
+    SqlConsoleError { errorMessage = message, errorDetail = fromMaybe "" detail, errorHint = fromMaybe "" hint, errorState = code }
+sessionErrorToConsoleError err =
+    SqlConsoleError { errorMessage = cs (HasqlErrors.toDetailedText err), errorDetail = "", errorHint = "", errorState = "" }
+
+-- | Reorder DynamicField results to match the column order from information_schema.
+-- The row_to_json → Aeson decoding returns fields in alphabetical order (KeyMap.toList),
+-- but views like EditRowView zip fields with tableCols which are in ordinal_position order.
+reorderFields :: [ColumnDefinition] -> [DynamicField] -> [DynamicField]
+reorderFields cols fields = map findField cols
+    where
+        findField col = fromMaybe (DynamicField { fieldName = cs col.columnName, fieldValue = Nothing }) $
+            List.find (\f -> f.fieldName == cs col.columnName) fields
 
 instance {-# OVERLAPS #-} ToJSON [DynamicField] where
     toJSON fields = object (map (\DynamicField { fieldName, fieldValue } -> (cs fieldName) .= (fieldValueToJSON fieldValue)) fields)
diff --git a/IHP/IDE/Data/View/EditRow.hs b/IHP/IDE/Data/View/EditRow.hs
--- a/IHP/IDE/Data/View/EditRow.hs
+++ b/IHP/IDE/Data/View/EditRow.hs
@@ -22,7 +22,7 @@
     html EditRowView { .. } = [hsx|
         <div class="h-100">
             {headerNav}
-            <div class="h-100 row no-gutters">
+            <div class="h-100 row g-0">
                 {renderTableSelector tableNames tableName}
                 <div class="col" style="overflow: scroll; max-height: 80vh">
                     {renderRows rows tableBody tableName}
@@ -37,7 +37,7 @@
                 where
                     id = (cs (fromMaybe "" ((fromJust (headMay fields)).fieldValue)))
             renderField id DynamicField { .. } | fieldName == "id" = [hsx|<td><span data-fieldname={fieldName}><a class="no-link border rounded p-1" href={EditRowValueAction tableName (cs fieldName) id}>{renderId (sqlValueToText fieldValue)}</a></span></td>|]
-            renderField id DynamicField { .. } | isBoolField fieldName tableCols && not (isNothing fieldValue) = [hsx|<td><span data-fieldname={fieldName}><input type="checkbox" onclick={onClick tableName fieldName id} checked={sqlValueToText fieldValue == "t"} /></span></td>|]
+            renderField id DynamicField { .. } | isBoolField fieldName tableCols && not (isNothing fieldValue) = [hsx|<td><span data-fieldname={fieldName}><input type="checkbox" onclick={onClick tableName fieldName id} checked={sqlValueToText fieldValue == "true"} /></span></td>|]
             renderField id DynamicField { .. } = [hsx|<td><span data-fieldname={fieldName}><a class="no-link" href={EditRowValueAction tableName (cs fieldName) id}>{sqlValueToText fieldValue}</a></span></td>|]
 
 
@@ -46,7 +46,7 @@
                     <input type="hidden" name="tableName" value={tableName}/>
                     {forEach (zip tableCols rowValues) renderFormField}
                     {forEach (zip primaryKeyFields (T.splitOn "---" targetPrimaryKey)) renderPrimaryKeyInput}
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Edit Row</button>
                     </div>
                 </form>
@@ -60,7 +60,7 @@
             
             renderFormField :: (ColumnDefinition, DynamicField) -> Html
             renderFormField (def, val) = [hsx|
-                    <div class="form-group">
+                    <div class="mb-3">
                         <label class="row-form">{def.columnName}</label>
                         <span style="float:right;">
                             <a class="text-muted row-form">{def.columnType}</a>
@@ -79,7 +79,7 @@
                                 id={def.columnName <> "-alt"}
                                 type="text"
                                 name={def.columnName}
-                                class="form-control text-monospace text-secondary bg-light"
+                                class="form-control font-monospace text-secondary bg-light"
                                 value="NULL"
                                 />
                             <div class="form-control" id={def.columnName <> "-boxcontainer"}>
@@ -88,7 +88,7 @@
                                     type="checkbox"
                                     class="d-none"
                                     name={def.columnName <> "-inactive"}
-                                    checked={(value val) == "t"}
+                                    checked={(value val) == "true"}
                                     />
                             </div>
                             <input
@@ -97,9 +97,8 @@
                                 name={def.columnName}
                                 value={inputValue False}
                                 />
-                            <div class="input-group-append">
-                                <button class="btn dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
-                                <div class="dropdown-menu dropdown-menu-right custom-menu menu-for-column shadow backdrop-blur">
+                                <button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
+                                <div class="dropdown-menu dropdown-menu-end custom-menu menu-for-column shadow backdrop-blur">
                                     <a class="dropdown-item" data-value="DEFAULT" data-issql="True" onclick={fillField def "DEFAULT" "true"}>DEFAULT</a>
                                     <a class="dropdown-item" data-value="NULL" data-issql="True" onclick={fillField def "NULL" "true"}>NULL</a>
                                     <a class="dropdown-item">
@@ -108,7 +107,7 @@
                                             type="checkbox"
                                             name={def.columnName <> "_"}
                                             checked={True}
-                                            class="mr-1"
+                                            class="me-1"
                                             onclick={"sqlModeCheckbox('" <> def.columnName <> "', this, true)"}
                                             />
                                         <label class="form-check-label" for={def.columnName <> "-sqlbox"}> Parse as SQL</label>
@@ -119,7 +118,6 @@
                                         value={inputValue False}
                                         />
                                 </div>
-                            </div>
                                 |]
             renderInputMethod (def, val) | (def.columnType) == "boolean" = [hsx|
                             {isBooleanParam True def}
@@ -127,14 +125,14 @@
                                 id={def.columnName <> "-alt"}
                                 type="text"
                                 name={def.columnName <> "-inactive"}
-                                class="form-control text-monospace text-secondary bg-light d-none"
+                                class="form-control font-monospace text-secondary bg-light d-none"
                                 />
                             <div class="form-control" id={def.columnName <> "-boxcontainer"}>
                                 <input
                                     id={def.columnName <> "-input"}
                                     type="checkbox"
                                     name={def.columnName}
-                                    checked={(value val) == "t"}
+                                    checked={(value val) == "true"}
                                     />
                             </div>
                             <input
@@ -143,9 +141,8 @@
                                 name={def.columnName}
                                 value={inputValue False}
                                 />
-                            <div class="input-group-append">
-                                <button class="btn dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
-                                <div class="dropdown-menu dropdown-menu-right custom-menu menu-for-column shadow backdrop-blur">
+                                <button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
+                                <div class="dropdown-menu dropdown-menu-end custom-menu menu-for-column shadow backdrop-blur">
                                     <a class="dropdown-item" data-value="DEFAULT" data-issql="True" onclick={fillField def "DEFAULT" "true"}>DEFAULT</a>
                                     <a class="dropdown-item" data-value="NULL" data-issql="True" onclick={fillField def "NULL" "true"}>NULL</a>
                                     <a class="dropdown-item">
@@ -154,7 +151,7 @@
                                             type="checkbox"
                                             name={def.columnName <> "_"}
                                             checked={isSqlFunction (getColDefaultValue def)}
-                                            class="mr-1"
+                                            class="me-1"
                                             onclick={"sqlModeCheckbox('" <> def.columnName <> "', this, true)"}
                                             />
                                         <label class="form-check-label" for={def.columnName <> "-sqlbox"}> Parse as SQL</label>
@@ -165,7 +162,6 @@
                                         value={inputValue False}
                                         />
                                 </div>
-                            </div>
                                 |]
             renderInputMethod (def, val) = [hsx|
                             {isBooleanParam False def}
@@ -173,13 +169,12 @@
                                 id={def.columnName <> "-input"}
                                 type="text"
                                 name={def.columnName}
-                                class={classes ["form-control", ("text-monospace text-secondary bg-light", isSqlFunction_ (value val))]}
+                                class={classes ["form-control", ("font-monospace text-secondary bg-light", isSqlFunction_ (value val))]}
                                 value={value val}
                                 oninput={"stopSqlModeOnInput('" <> def.columnName <> "')"}
                                 />
-                            <div class="input-group-append">
-                                <button class="btn dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
-                                <div class="dropdown-menu dropdown-menu-right custom-menu menu-for-column shadow backdrop-blur">
+                                <button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
+                                <div class="dropdown-menu dropdown-menu-end custom-menu menu-for-column shadow backdrop-blur">
                                     <a class="dropdown-item" data-value="DEFAULT" data-issql="True" onclick={fillField def "DEFAULT" "false"}>DEFAULT</a>
                                     <a class="dropdown-item" data-value="NULL" data-issql="True" onclick={fillField def "NULL" "false"}>NULL</a>
                                     <a class="dropdown-item">
@@ -188,7 +183,7 @@
                                             type="checkbox"
                                             name={def.columnName <> "_"}
                                             checked={isSqlFunction_ (value val)}
-                                            class="mr-1"
+                                            class="me-1"
                                             onclick={"sqlModeCheckbox('" <> def.columnName <> "', this)"}
                                             />
                                         <label class="form-check-label" for={def.columnName <> "-sqlbox"}> Parse as SQL</label>
@@ -199,6 +194,6 @@
                                         value={inputValue False}
                                         />
                                 </div>
-                            </div>|]
+                            |]
 
 value val = fromMaybe BS.empty (val.fieldValue)
diff --git a/IHP/IDE/Data/View/EditValue.hs b/IHP/IDE/Data/View/EditValue.hs
--- a/IHP/IDE/Data/View/EditValue.hs
+++ b/IHP/IDE/Data/View/EditValue.hs
@@ -18,7 +18,7 @@
     html EditValueView { .. } = [hsx|
         <div class="h-100">
             {headerNav}
-            <div class="h-100 row no-gutters">
+            <div class="h-100 row g-0">
                 {renderTableSelector tableNames tableName}
                 <div class="col" style="overflow: scroll; max-height: 80vh">
                     {renderRows rows tableBody tableName}
@@ -35,7 +35,7 @@
             </tr>
             <div class="custom-menu menu-for-column shadow backdrop-blur" id={contextMenuId}>
                 <a href={EditRowAction tableName id}>Edit Row</a>
-                <a href={DeleteEntryAction tableName id} class="js-delete">Delete Row</a>
+                <a href={DeleteEntryAction id tableName} class="js-delete">Delete Row</a>
                 <div></div>
                 <a href={NewRowAction tableName}>Add Row</a>
             </div>|]
diff --git a/IHP/IDE/Data/View/Layout.hs b/IHP/IDE/Data/View/Layout.hs
--- a/IHP/IDE/Data/View/Layout.hs
+++ b/IHP/IDE/Data/View/Layout.hs
@@ -35,9 +35,6 @@
                         <a
                             href={NewRowAction tableName}
                             class="btn btn-link btn-add"
-                            data-toggle="tooltip"
-                            data-placement="bottom"
-                            title={"Add " <> tableNameToModelName tableName}
                         >{addIcon}</a>
                     </div>
                 </th>
@@ -67,16 +64,16 @@
     Nothing -> False
 
 isSqlFunction :: Text -> Bool
-isSqlFunction text = text `elem`
-    [ "uuid_generate_v4()"
-    , "NOW()"
-    , "NULL"]
+isSqlFunction text = Text.toLower text `elem`
+    [ "uuidv7()"
+    , "uuidv4()"
+    , "uuid_generate_v4()"
+    , "now()"
+    , "null"
+    , "default"]
 
 isSqlFunction_ :: ByteString -> Bool
-isSqlFunction_ text = text `elem`
-    [ "uuid_generate_v4()"
-    , "NOW()"
-    , "NULL"]
+isSqlFunction_ text = isSqlFunction (cs text)
 
 fillField col value isBoolField = "fillField('" <> col.columnName <> "', '" <> value <> "'," <> isBoolField <> ");"
 
diff --git a/IHP/IDE/Data/View/NewRow.hs b/IHP/IDE/Data/View/NewRow.hs
--- a/IHP/IDE/Data/View/NewRow.hs
+++ b/IHP/IDE/Data/View/NewRow.hs
@@ -18,7 +18,7 @@
     html NewRowView { .. } = [hsx|
         <div class="h-100">
             {headerNav}
-            <div class="h-100 row no-gutters">
+            <div class="h-100 row g-0">
                 {renderTableSelector tableNames tableName}
                 <div class="col" style="overflow: scroll; max-height: 80vh">
                     {renderRows rows tableBody tableName}
@@ -33,7 +33,7 @@
                 where
                     id = (cs (fromMaybe "" ((fromJust (headMay fields)).fieldValue)))
             renderField id DynamicField { .. } | fieldName == "id" = [hsx|<td><span data-fieldname={fieldName}><a class="no-link border rounded p-1" href={EditRowValueAction tableName (cs fieldName) id}>{renderId (sqlValueToText fieldValue)}</a></span></td>|]
-            renderField id DynamicField { .. } | isBoolField fieldName tableCols && not (isNothing fieldValue) = [hsx|<td><span data-fieldname={fieldName}><input type="checkbox" onclick={onClick tableName fieldName id} checked={sqlValueToText fieldValue == "t"} /></span></td>|]
+            renderField id DynamicField { .. } | isBoolField fieldName tableCols && not (isNothing fieldValue) = [hsx|<td><span data-fieldname={fieldName}><input type="checkbox" onclick={onClick tableName fieldName id} checked={sqlValueToText fieldValue == "true"} /></span></td>|]
             renderField id DynamicField { .. } = [hsx|<td><span data-fieldname={fieldName}><a class="no-link" href={EditRowValueAction tableName (cs fieldName) id}>{sqlValueToText fieldValue}</a></span></td>|]
 
             modalContent = [hsx|
@@ -41,7 +41,7 @@
                     <input type="hidden" name="tableName" value={tableName}/>
                     {forEach tableCols renderFormField}
                     {renderFlashMessages}
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Add Row</button>
                     </div>
                 </form>
@@ -52,13 +52,13 @@
             modal = Modal { modalContent, modalFooter, modalCloseUrl, modalTitle }
 
             renderFormField col = [hsx|
-                    <div class="form-group">
+                    <div class="mb-3">
                         <label class="row-form">{col.columnName}</label>
                         <span style="float:right;">
                             <a class="text-muted row-form">{col.columnType}</a>
                         </span>
 
-                        <div class="d-flex">
+                        <div class="input-group">
                             {renderInputMethod col}
                         </div>
                     </div>|]
@@ -71,7 +71,7 @@
                                 id={col.columnName <> "-alt"}
                                 type="text"
                                 name={col.columnName <> "-inactive"}
-                                class="form-control text-monospace text-secondary d-none"
+                                class="form-control font-monospace text-secondary d-none"
                                 />
                             <div class="form-control" id={col.columnName <> "-boxcontainer"}>
                                 <input
@@ -87,9 +87,8 @@
                                 name={col.columnName}
                                 value={inputValue False}
                                 />
-                            <div class="input-group-append">
-                                <button class="btn dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
-                                <div class="dropdown-menu dropdown-menu-right custom-menu menu-for-column shadow backdrop-blur">
+                                <button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
+                                <div class="dropdown-menu dropdown-menu-end custom-menu menu-for-column shadow backdrop-blur">
                                     <a class="dropdown-item" data-value="DEFAULT" data-issql="True" onclick={fillField col "DEFAULT" "true"}>DEFAULT</a>
                                     <a class="dropdown-item" data-value="NULL" data-issql="True" onclick={fillField col "NULL" "true"}>NULL</a>
                                     <a class="dropdown-item">
@@ -98,7 +97,7 @@
                                             type="checkbox"
                                             name={col.columnName <> "_"}
                                             checked={isSqlFunction (getColDefaultValue col)}
-                                            class="mr-1"
+                                            class="me-1"
                                             onclick={"sqlModeCheckbox('" <> col.columnName <> "', this, true)"}
                                             />
                                         <label class="form-check-label" for={col.columnName <> "-sqlbox"}> Parse as SQL</label>
@@ -109,7 +108,6 @@
                                         value={inputValue False}
                                         />
                                 </div>
-                            </div>
                                 |]
             renderInputMethod col = [hsx|
                                 {isBooleanParam False col}
@@ -117,8 +115,8 @@
                                         then select
                                         else theInput
                                 }
-                                <button class="btn dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
-                                <div class="dropdown-menu dropdown-menu-right custom-menu menu-for-column shadow backdrop-blur">
+                                <button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
+                                <div class="dropdown-menu dropdown-menu-end custom-menu menu-for-column shadow backdrop-blur">
                                     <a class="dropdown-item" data-value="DEFAULT" data-issql="True" onclick={fillField col "DEFAULT" "false"}>DEFAULT</a>
                                     <a class="dropdown-item" data-value="NULL" data-issql="True" onclick={fillField col "NULL" "false"}>NULL</a>
                                     <a class="dropdown-item">
@@ -127,7 +125,7 @@
                                             type="checkbox"
                                             name={col.columnName <> "_"}
                                             checked={isSqlFunction (getColDefaultValue col)}
-                                            class="mr-1"
+                                            class="me-1"
                                             onclick={"sqlModeCheckbox('" <> col.columnName <> "', this, false)"}
                                             />
                                         <label class="form-check-label" for={col.columnName <> "-sqlbox"}> Parse as SQL</label>
@@ -150,7 +148,7 @@
                                             id={col.columnName <> "-input"}
                                             type="text"
                                             name={col.columnName}
-                                            class={classes ["form-control", ("text-monospace", isSqlFunction (getColDefaultValue col)), ("is-foreign-key-column", isForeignKeyColumn)]}
+                                            class={classes ["form-control", ("font-monospace", isSqlFunction (getColDefaultValue col)), ("is-foreign-key-column", isForeignKeyColumn)]}
                                             value={renderDefaultWithoutType (getColDefaultValue col)}
                                             oninput={"stopSqlModeOnInput('" <> col.columnName <> "')"}
                                         />
diff --git a/IHP/IDE/Data/View/ShowDatabase.hs b/IHP/IDE/Data/View/ShowDatabase.hs
--- a/IHP/IDE/Data/View/ShowDatabase.hs
+++ b/IHP/IDE/Data/View/ShowDatabase.hs
@@ -12,7 +12,7 @@
     html ShowDatabaseView { .. } = [hsx|
         <div class="h-100">
             {headerNav}
-            <div class="h-100 row no-gutters" oncontextmenu="event.preventDefault();">
+            <div class="h-100 row g-0" oncontextmenu="event.preventDefault();">
                 {renderTableSelector tableNames ""}
             </div>
         </div>
@@ -23,7 +23,7 @@
 renderTableSelector tableNames activeTableName = [hsx|
     <div class="col-2 object-selector" oncontextmenu="event.preventDefault();">
         <div class="d-flex">
-            <h5 class="pl-3">Tables</h5>
+            <h5 class="ps-3">Tables</h5>
         </div>
         {forEach tableNames renderTable}
         <div class="text-muted context-menu-notice">Right click to open context menu</div>
@@ -34,7 +34,7 @@
         renderTable name = [hsx|
             <a
                 href={ShowTableRowsAction name}
-                class={classes [("object object-table w-100 context-table pl-3", True), ("active", name == activeTableName)]}
+                class={classes [("object object-table w-100 context-table ps-3", True), ("active", name == activeTableName)]}
                 oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}
             >
                 {name}
diff --git a/IHP/IDE/Data/View/ShowQuery.hs b/IHP/IDE/Data/View/ShowQuery.hs
--- a/IHP/IDE/Data/View/ShowQuery.hs
+++ b/IHP/IDE/Data/View/ShowQuery.hs
@@ -1,12 +1,11 @@
 module IHP.IDE.Data.View.ShowQuery where
 
-import qualified Database.PostgreSQL.Simple as PG
 import IHP.ViewPrelude
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.Data.View.Layout
 
 data ShowQueryView = ShowQueryView
-    { queryResult :: Maybe (Either PG.SqlError SqlConsoleResult)
+    { queryResult :: Maybe (Either SqlConsoleError SqlConsoleResult)
     , queryText :: Text
     }
 
@@ -24,8 +23,8 @@
 
                     <button
                         class="btn btn-primary"
-                        data-toggle="tooltip"
-                        data-placement="right"
+                        data-bs-toggle="tooltip"
+                        data-bs-placement="right"
                         title="⌘ Enter"
                     >Run SQL Query</button>
                 </form>
@@ -55,11 +54,11 @@
                 |]
                 Just (Left sqlError) -> [hsx|
                     <div class="alert alert-danger" role="alert">
-                        <h4 class="alert-heading">SQL Error - {sqlError.sqlExecStatus}</h4>
-                        {showIfNotEmpty "Message" (sqlError.sqlErrorMsg)}
-                        {showIfNotEmpty "Details" (sqlError.sqlErrorDetail)}
-                        {showIfNotEmpty "Hint" (sqlError.sqlErrorHint)}
-                        {showIfNotEmpty "State" (sqlError.sqlState)}
+                        <h4 class="alert-heading">SQL Error</h4>
+                        {showIfNotEmpty "Message" (sqlError.errorMessage)}
+                        {showIfNotEmpty "Details" (sqlError.errorDetail)}
+                        {showIfNotEmpty "Hint" (sqlError.errorHint)}
+                        {showIfNotEmpty "State" (sqlError.errorState)}
                     </div>
                 |]
                 Nothing -> mempty
@@ -73,7 +72,7 @@
 
             columnNames rows = maybe [] (map (.fieldName)) (head rows)
 
-            showIfNotEmpty :: Text -> ByteString -> Html
+            showIfNotEmpty :: Text -> Text -> Html
             showIfNotEmpty title = \case
                 "" -> mempty
                 text -> [hsx|<div><strong>{title}:</strong> {text}</div>|]
diff --git a/IHP/IDE/Data/View/ShowTableRows.hs b/IHP/IDE/Data/View/ShowTableRows.hs
--- a/IHP/IDE/Data/View/ShowTableRows.hs
+++ b/IHP/IDE/Data/View/ShowTableRows.hs
@@ -21,7 +21,7 @@
     html ShowTableRowsView { .. } = [hsx|
         <div class="h-100">
             {headerNav}
-            <div class="h-100 row no-gutters">
+            <div class="h-100 row g-0">
                 {renderTableSelector tableNames tableName}
                 <div class="col" oncontextmenu="showContextMenu('context-menu-data-root')">
                     <div style="overflow: scroll; max-height: 80vh">
@@ -52,7 +52,7 @@
                     primaryKey = intercalate "---" . map (cs . fromMaybe "" . (.fieldValue)) $ filter ((`elem` primaryKeyFields) . cs . (.fieldName)) fields
             renderField primaryKey DynamicField { .. }
                 | fieldName == "id" = [hsx|<td><span data-fieldname={fieldName}><a class="border rounded p-1" href={EditRowValueAction tableName (cs fieldName) primaryKey}>{renderId (sqlValueToText fieldValue)}</a></span></td>|]
-                | isBoolField fieldName tableCols && not (isNothing fieldValue) = [hsx|<td><span data-fieldname={fieldName}><input type="checkbox" onclick={onClick tableName fieldName primaryKey} checked={sqlValueToText fieldValue == "t"} /></span></td>|]
+                | isBoolField fieldName tableCols && not (isNothing fieldValue) = [hsx|<td><span data-fieldname={fieldName}><input type="checkbox" onclick={onClick tableName fieldName primaryKey} checked={sqlValueToText fieldValue == "true"} /></span></td>|]
                 | otherwise = renderNormalField primaryKey DynamicField { .. }
 
             renderNormalField primaryKey DynamicField { .. } = [hsx|
@@ -101,7 +101,7 @@
                         
 
             renderPageButton :: Int -> Html
-            renderPageButton nr = [hsx|<a href={pathTo (ShowTableRowsAction tableName) <> "&page=" <> show nr <> "&rows=" <> show pageSize} class={classes ["mx-2", (if page==nr then "text-dark font-weight-bold" else "text-muted")]}>{nr}</a>|]
+            renderPageButton nr = [hsx|<a href={pathTo (ShowTableRowsAction tableName) <> "&page=" <> show nr <> "&rows=" <> show pageSize} class={classes ["mx-2", (if page==nr then "text-dark fw-bold" else "text-muted")]}>{nr}</a>|]
 
             emptyState :: Html
             emptyState = [hsx|
diff --git a/IHP/IDE/FileWatcher.hs b/IHP/IDE/FileWatcher.hs
--- a/IHP/IDE/FileWatcher.hs
+++ b/IHP/IDE/FileWatcher.hs
@@ -5,11 +5,14 @@
 import Control.Concurrent (threadDelay)
 import Control.Concurrent.MVar
 import Control.Monad (filterM)
-import System.Directory (listDirectory, doesDirectoryExist)
+import qualified System.Directory.OsPath as Directory
 import qualified Data.Map as Map
 import qualified System.FSNotify as FS
 import qualified Data.List as List
 import qualified Control.Debounce as Debounce
+import System.OsPath (encodeUtf, decodeUtf)
+import qualified System.Process as Process
+import qualified System.IO as IO
 
 data FileWatcherParams
     = FileWatcherParams
@@ -81,16 +84,39 @@
 
 listWatchableDirectories :: IO [String]
 listWatchableDirectories = do
-    rootDirectoryContents <- listDirectory "."
-    filterM shouldWatchDirectory rootDirectoryContents
+    osEntries <- Directory.listDirectory "."
+    rootDirectoryContents <- mapM decodeUtf osEntries
+    directories <- filterM shouldWatchDirectory rootDirectoryContents
+    filterGitIgnored directories
 
 shouldWatchDirectory :: String -> IO Bool
 shouldWatchDirectory path = do
-    isDirectory <- doesDirectoryExist path
-    pure $ isDirectory && isDirectoryWatchable path
+    osPath <- encodeUtf path
+    Directory.doesDirectoryExist osPath
 
+-- | Filter out directories that are git-ignored.
+-- Falls back to the old hardcoded exclusion list if git is not available
+-- or the project is not a git repo.
+filterGitIgnored :: [String] -> IO [String]
+filterGitIgnored [] = pure []
+filterGitIgnored dirs = do
+    result <- try @SomeException $ do
+        let process = (Process.proc "git" ("check-ignore" : "--no-index" : dirs))
+                { Process.std_out = Process.CreatePipe
+                , Process.std_err = Process.CreatePipe
+                }
+        Process.withCreateProcess process \_ (Just stdout) _ processHandle -> do
+            output <- IO.hGetContents stdout
+            _ <- evaluate (length output)
+            let ignored = List.lines output
+            _ <- Process.waitForProcess processHandle
+            pure ignored
+    case result of
+        Right ignored -> pure (filter (`notElem` ignored) dirs)
+        Left _ -> pure (filter isDirectoryWatchable dirs)
+
 isDirectoryWatchable :: String -> Bool
-isDirectoryWatchable path = 
+isDirectoryWatchable path =
     path /= ".devenv" && path /= ".direnv"
 
 fileWatcherConfig :: FS.WatchConfig
diff --git a/IHP/IDE/LiveReloadNotificationServer.hs b/IHP/IDE/LiveReloadNotificationServer.hs
--- a/IHP/IDE/LiveReloadNotificationServer.hs
+++ b/IHP/IDE/LiveReloadNotificationServer.hs
@@ -3,7 +3,6 @@
 import IHP.Prelude
 import qualified Network.WebSockets as Websocket
 import qualified Control.Concurrent as Concurrent
-import IHP.IDE.Types
 import qualified Control.Exception as Exception
 import qualified Data.UUID.V4 as UUID
 import qualified Data.Map as Map
diff --git a/IHP/IDE/Logs/Controller.hs b/IHP/IDE/Logs/Controller.hs
--- a/IHP/IDE/Logs/Controller.hs
+++ b/IHP/IDE/Logs/Controller.hs
@@ -4,10 +4,10 @@
 import IHP.IDE.ToolServer.Helper.Controller
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.Logs.View.Logs
-import qualified IHP.IDE.Types as DevServer
+import IHP.IDE.Logs.ServiceLog (discoverServices, getServiceLogs)
 import qualified Data.ByteString.Char8 as ByteString
-import qualified Data.ByteString.Builder as ByteString
-import qualified Control.Concurrent.MVar as MVar
+import qualified IHP.EnvVar as EnvVar
+import qualified System.Directory as Directory
 
 instance Controller LogsController where
     action AppLogsAction = do
@@ -16,13 +16,32 @@
         standardOutput <- cs . ByteString.unlines . reverse <$> readIORef toolServerApp.appStandardOutput
         errorOutput <- cs . ByteString.unlines . reverse <$> readIORef toolServerApp.appErrorOutput
 
+        services <- discoverServices
+        let activeService = "app"
+
         render LogsView { .. }
 
     action PostgresLogsAction = do
-        toolServerApp <- fromContext @ToolServerApplication
+        pgdata <- EnvVar.env @String "PGDATA"
+        let logFile = pgdata <> "/log/postgresql.log"
 
-        standardOutput <- cs . ByteString.toLazyByteString <$> readIORef toolServerApp.postgresStandardOutput
-        errorOutput <- cs . ByteString.toLazyByteString <$> readIORef toolServerApp.postgresErrorOutput
+        logExists <- Directory.doesFileExist logFile
+        standardOutput <- if logExists
+            then cs <$> ByteString.readFile logFile
+            else pure ("Postgres log file not found" :: ByteString)
+        let errorOutput = "" :: ByteString
+
+        services <- discoverServices
+        let activeService = "postgres"
+
+        render LogsView { .. }
+
+    action ServiceLogsAction { serviceName } = do
+        standardOutput <- cs <$> getServiceLogs serviceName
+        let errorOutput = "" :: ByteString
+
+        services <- discoverServices
+        let activeService = serviceName
 
         render LogsView { .. }
 
diff --git a/IHP/IDE/Logs/ServiceLog.hs b/IHP/IDE/Logs/ServiceLog.hs
new file mode 100644
--- /dev/null
+++ b/IHP/IDE/Logs/ServiceLog.hs
@@ -0,0 +1,157 @@
+-- | Service discovery and log reading for devenv process managers.
+--
+-- Supports process-compose (via @$PC_CONFIG_FILES@) and overmind (via Procfile).
+-- Log lines are read from the process manager's combined log file and filtered
+-- by the @"serviceName |"@ prefix each manager prepends.
+module IHP.IDE.Logs.ServiceLog
+    ( discoverServices
+    , getServiceLogs
+    -- * Pure helpers (exported for testing)
+    , extractProcessComposeNames
+    , filterServiceLines
+    , filterBuiltinServices
+    ) where
+
+import IHP.Prelude
+import qualified IHP.EnvVar as EnvVar
+import qualified System.Directory as Directory
+import qualified Data.Text as Text
+import qualified Data.Text.IO as Text.IO
+
+-- | Discover devenv services by checking process-compose config or Procfile.
+-- Returns service names excluding "ihp" and "postgres" (which have dedicated tabs).
+-- Returns @[]@ if no config is found or files are unreadable.
+discoverServices :: IO [Text]
+discoverServices = do
+    pcConfig <- EnvVar.envOrNothing "PC_CONFIG_FILES" :: IO (Maybe String)
+    case pcConfig of
+        Just configPath -> discoverFromProcessCompose configPath
+        Nothing -> do
+            procfileExists <- Directory.doesFileExist "Procfile"
+            if procfileExists
+                then discoverFromProcfile "Procfile"
+                else pure []
+
+-- | Read logs for a specific devenv service from the process manager log file.
+-- Filters lines by the @"serviceName |"@ prefix pattern and returns the last 10 000 lines.
+getServiceLogs :: Text -> IO ByteString
+getServiceLogs serviceName = do
+    logFile <- findProcessManagerLogFile
+    case logFile of
+        Just path -> do
+            exists <- Directory.doesFileExist path
+            if exists
+                then do
+                    content <- Text.IO.readFile path
+                    let filtered = filterServiceLines serviceName (Text.lines content)
+                    let limited = takeLast 10000 filtered
+                    pure (cs (Text.unlines limited))
+                else pure ("Log file not found: " <> cs path)
+        Nothing -> pure "No process manager log file found"
+
+------------------------------------------------------------------------
+-- Service discovery
+------------------------------------------------------------------------
+
+discoverFromProcessCompose :: String -> IO [Text]
+discoverFromProcessCompose configPath = do
+    -- PC_CONFIG_FILES can contain multiple paths separated by ","
+    let firstPath = takeWhile (/= ',') configPath
+    exists <- Directory.doesFileExist firstPath
+    if exists
+        then do
+            content <- Text.IO.readFile firstPath
+            pure (filterBuiltinServices (extractProcessComposeNames (Text.lines content)))
+        else pure []
+
+discoverFromProcfile :: String -> IO [Text]
+discoverFromProcfile path = do
+    content <- Text.IO.readFile path
+    let names = mapMaybe parseProcfileLine (Text.lines content)
+    pure (filterBuiltinServices names)
+    where
+        parseProcfileLine line =
+            let stripped = Text.strip line
+            in if Text.null stripped || Text.isPrefixOf "#" stripped
+                then Nothing
+                else case Text.breakOn ":" stripped of
+                    (name, rest')
+                        | not (Text.null rest'), not (Text.null name)
+                        -> Just (Text.strip name)
+                    _ -> Nothing
+
+------------------------------------------------------------------------
+-- Log file location
+------------------------------------------------------------------------
+
+findProcessManagerLogFile :: IO (Maybe String)
+findProcessManagerLogFile = do
+    devenvState <- EnvVar.envOrNothing "DEVENV_STATE" :: IO (Maybe String)
+    let baseDirs = maybe [".devenv/state"] (\s -> [s, ".devenv/state"]) devenvState
+    findFirst
+        [ dir <> suffix
+        | dir <- baseDirs
+        , suffix <- ["/process-compose/process-compose.log", "/overmind.log"]
+        ]
+    where
+        findFirst [] = pure Nothing
+        findFirst (p:ps) = do
+            exists <- Directory.doesFileExist p
+            if exists then pure (Just p) else findFirst ps
+
+------------------------------------------------------------------------
+-- Pure helpers
+------------------------------------------------------------------------
+
+-- | Extract process names from process-compose YAML.
+-- Finds the @processes:@ section and extracts direct child keys only,
+-- ignoring deeper-nested keys like @command:@ or @depends_on:@.
+extractProcessComposeNames :: [Text] -> [Text]
+extractProcessComposeNames ls = go Nothing ls
+    where
+        go _ [] = []
+        go state (l:rest)
+            | Text.stripEnd l == "processes:" = go (Just Nothing) rest
+            | Nothing <- state = go Nothing rest
+            | not (Text.null l), not (Text.isPrefixOf " " l), not (Text.isPrefixOf "\t" l) = []
+            | Just mIndent <- state
+            , Just (indent, name) <- parseIndentedKey l
+            = case mIndent of
+                Nothing -> name : go (Just (Just indent)) rest
+                Just expected
+                    | indent == expected -> name : go state rest
+                    | indent < expected  -> []
+                    | otherwise          -> go state rest
+            | otherwise = go state rest
+
+        parseIndentedKey line =
+            let indent = Text.length (Text.takeWhile (== ' ') line)
+                stripped = Text.strip line
+            in if indent == 0 || Text.null stripped || Text.isPrefixOf "#" stripped
+                then Nothing
+                else case Text.breakOn ":" stripped of
+                    (name, rest')
+                        | not (Text.null rest'), not (Text.null name), Text.all isNameChar name
+                        -> Just (indent, name)
+                    _ -> Nothing
+
+        isNameChar c = c == '_' || c == '-' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
+
+-- | Filter log lines belonging to a specific service.
+-- Matches the @"name  |"@ or @"name |"@ prefix that process-compose and overmind prepend.
+filterServiceLines :: Text -> [Text] -> [Text]
+filterServiceLines name = mapMaybe extractLine
+    where
+        extractLine line =
+            case Text.breakOn "|" (Text.stripStart line) of
+                (prefix, rest')
+                    | not (Text.null rest'), Text.strip prefix == name
+                    -> Just (Text.drop 1 rest')
+                _ -> Nothing
+
+-- | Filter out services that already have dedicated tabs.
+filterBuiltinServices :: [Text] -> [Text]
+filterBuiltinServices = filter (\name -> Text.toLower name /= "ihp" && Text.toLower name /= "postgres")
+
+takeLast :: Int -> [a] -> [a]
+takeLast n xs = drop (max 0 (length xs - n)) xs
diff --git a/IHP/IDE/Logs/View/Logs.hs b/IHP/IDE/Logs/View/Logs.hs
--- a/IHP/IDE/Logs/View/Logs.hs
+++ b/IHP/IDE/Logs/View/Logs.hs
@@ -4,17 +4,40 @@
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.ToolServer.Layout ()
 
-data LogsView = LogsView { standardOutput :: ByteString, errorOutput :: ByteString }
+data LogsView = LogsView
+    { standardOutput :: ByteString
+    , errorOutput :: ByteString
+    , services :: [Text]
+    , activeService :: Text
+    }
 
 instance View LogsView where
     html LogsView { .. } = [hsx|
         <div id="logs">
-            <div class="logs-navigation">
-                <a href={AppLogsAction} class={classes [("active", isActivePath AppLogsAction)]}>App</a>
-                <a href={PostgresLogsAction} class={classes [("active", isActivePath PostgresLogsAction)]}>Postgres</a>
+            <div class="logs-tab-bar">
+                <a href={AppLogsAction} class={classes [("logs-tab", True), ("active", activeService == "app")]}>App</a>
+                <a href={PostgresLogsAction} class={classes [("logs-tab", True), ("active", activeService == "postgres")]}>Postgres</a>
+                {forEach services serviceTab}
             </div>
 
-            <pre>{standardOutput}</pre>
-            <pre>{errorOutput}</pre>
+            <div class="logs-output">
+                <pre>{standardOutput}</pre>
+                {renderStderr}
+            </div>
         </div>
+        <script>
+            document.addEventListener('turbolinks:load', function() {
+                var output = document.querySelector('.logs-output');
+                if (output) output.scrollTop = output.scrollHeight;
+            });
+        </script>
     |]
+        where
+            renderStderr = if errorOutput /= ""
+                then [hsx|<pre class="stderr">{errorOutput}</pre>|]
+                else mempty
+
+            serviceTab :: Text -> Html
+            serviceTab name = [hsx|
+                <a href={ServiceLogsAction name} class={classes [("logs-tab", True), ("active", activeService == name)]}>{name}</a>
+            |]
diff --git a/IHP/IDE/PortConfig.hs b/IHP/IDE/PortConfig.hs
--- a/IHP/IDE/PortConfig.hs
+++ b/IHP/IDE/PortConfig.hs
@@ -3,6 +3,7 @@
 , defaultAppPort
 , findAvailablePortConfig
 , isPortAvailable
+, createListeningSocket
 )
 where
 
@@ -10,9 +11,10 @@
 import qualified Network.Socket as Socket
 import qualified UnliftIO.Exception as Exception
 import Foreign.C.Error (Errno (..), eCONNREFUSED)
-import Control.Exception.Safe (IOException(..))
 import GHC.IO.Exception (ioe_errno)
 import IHP.FrameworkConfig (defaultPort)
+import qualified System.Posix.IO as Posix
+import System.Posix.Types (Fd(..))
 
 -- | Port configuration used for starting the different app services
 data PortConfig = PortConfig
@@ -76,3 +78,19 @@
                 then pure portConfig
                 else go rest
         go [] = error "findAvailablePortConfig: No port configuration found"
+
+-- | Creates a listening socket bound to the given port on localhost.
+-- The socket is set up with SO_REUSEADDR and a listen backlog of 1024.
+-- This socket can be shared between the status server and the app server
+-- to ensure seamless transitions during app restarts.
+-- The CLOEXEC flag is cleared so child processes (like GHCi) can inherit the socket.
+createListeningSocket :: Socket.PortNumber -> IO Socket.Socket
+createListeningSocket port = do
+    socket <- Socket.socket Socket.AF_INET Socket.Stream Socket.defaultProtocol
+    Socket.setSocketOption socket Socket.ReuseAddr 1
+    Socket.bind socket (Socket.SockAddrInet port (Socket.tupleToHostAddress (127, 0, 0, 1)))
+    Socket.listen socket 1024
+    -- Clear the CLOEXEC flag so child processes (GHCi) can inherit this socket FD
+    fd <- Socket.unsafeFdSocket socket
+    Posix.setFdOption (Fd (fromIntegral fd)) Posix.CloseOnExec False
+    pure socket
diff --git a/IHP/IDE/Postgres.hs b/IHP/IDE/Postgres.hs
--- a/IHP/IDE/Postgres.hs
+++ b/IHP/IDE/Postgres.hs
@@ -1,157 +1,25 @@
-module IHP.IDE.Postgres (withPostgres, withBuiltinOrDevenvPostgres) where
+module IHP.IDE.Postgres (waitPostgres) where
 
 import IHP.IDE.Types
 import IHP.Prelude
-import qualified System.Process as Process
-import qualified System.Directory as Directory
-import qualified Data.ByteString.Char8 as ByteString
-import qualified Data.ByteString.Builder as ByteString
 import Control.Concurrent (threadDelay)
-import Control.Concurrent.MVar
-import GHC.IO.Handle
-import qualified Control.Exception.Safe as Exception
 
 import qualified IHP.Log as Log
 import qualified IHP.EnvVar as EnvVar
-import Paths_ihp_ide (getDataFileName)
 
-withPostgres :: (?context :: Context) => (MVar () -> IORef ByteString.Builder -> IORef ByteString.Builder -> IO a) -> IO a
-withPostgres callback = do
-    currentDir <- Directory.getCurrentDirectory
-    ensureNoOtherPostgresIsRunning
-    shouldInit <- needsDatabaseInit
-    when shouldInit initDatabase
-
-    Process.withCreateProcess (postgresProcessParams currentDir) \(Just inputHandle) (Just outputHandle) (Just errorHandle) processHandle -> do
-        let main = do
-                standardOutput <- newIORef mempty
-                errorOutput <- newIORef mempty
-                databaseIsReady <- newEmptyMVar
-
-                redirectHandleToVariable standardOutput outputHandle handleOutdatedDatabase
-                redirectHandleToVariable errorOutput errorHandle (handleOutdatedDatabase >> handleDatabaseReady databaseIsReady)
-
-                callback databaseIsReady standardOutput errorOutput
-
-        Exception.finally main (softStopPostgres processHandle)
-
-softStopPostgres :: Process.ProcessHandle -> IO ()
-softStopPostgres processHandle = do
-    let interruptAndWait = Process.interruptProcessGroupOf processHandle >> Process.waitForProcess processHandle
-    let waitAndKill = threadDelay 1000000 >> pure ()
-    race_
-        interruptAndWait
-        waitAndKill
-
-postgresProcessParams :: (?context :: Context) => FilePath -> Process.CreateProcess
-postgresProcessParams workingDirectory =
-    let
-        args = ["-D", "build/db/state", "-k", workingDirectory <> "/build/db", "-c", "listen_addresses="]
-    in (procDirenvAware "postgres" args)
-        { Process.std_in = Process.CreatePipe
-        , Process.std_out = Process.CreatePipe
-        , Process.std_err = Process.CreatePipe
-        , Process.create_group = True
-        }
-
-handleDatabaseReady :: MVar () -> ByteString -> IO ()
-handleDatabaseReady onReady line = when ("database system is ready to accept connections" `ByteString.isInfixOf` line) (putMVar onReady ())
-
-handleOutdatedDatabase :: (?context :: Context) => ByteString -> IO ()
-handleOutdatedDatabase line =
-        -- Always log fatal errors to the output:
-        -- 2021-09-04 12:18:08.888 CEST [55794] FATAL:  database files are incompatible with server
-        --
-        -- If we're in debug mode, log all output
-        if "FATAL" `ByteString.isInfixOf` line
-            then if "database files are incompatible with server" `ByteString.isInfixOf` line
-                then Log.error ("The current database state has been created with a different postgres server. Likely you just upgraded the IHP version. Delete your local dev database with 'rm -rf build/db'. You can use 'make dumpdb' to save your database state to Fixtures.sql, otherwise all changes in your local db will be lost. After that run 'devenv up' again." :: Text)
-                else Log.error line
-            else when ?context.isDebugMode (Log.debug line)
-
-redirectHandleToVariable :: IORef ByteString.Builder -> Handle -> (ByteString -> IO ()) -> IO (Async ())
-redirectHandleToVariable !ref !handle !onLine = do
-    async $ forever $ do
-        line <- ByteString.hGetLine handle
-        onLine line
-        modifyIORef ref (\log -> log <> "\n" <> ByteString.byteString line)
-
-ensureNoOtherPostgresIsRunning :: IO ()
-ensureNoOtherPostgresIsRunning = do
-    pidFileExists <- Directory.doesPathExist "build/db/state/postmaster.pid"
-    let stopFailedHandler (exception :: SomeException) = do
-            -- pg_ctl: could not send stop signal (PID: 123456765432): No such process
-            if ("No such process" `isInfixOf` (tshow exception))
-                then Directory.removeFile "build/db/state/postmaster.pid"
-                else putStrLn "Found postgres lockfile at 'build/db/state/postmaster.pid'. Could not bring the other postgres instance to halt. Please stop the running postgres manually and then restart this dev server"
-    when pidFileExists do
-        (Process.callProcess "pg_ctl" ["stop", "-D", "build/db/state"]) `catch` stopFailedHandler
-
-needsDatabaseInit :: IO Bool
-needsDatabaseInit = not <$> Directory.doesDirectoryExist "build/db/state"
-
-initDatabase :: IO ()
-initDatabase = do
-    currentDir <- Directory.getCurrentDirectory
-    Directory.createDirectoryIfMissing True "build/db"
-
-    Process.callProcess "initdb" [
-                "build/db/state"
-                , "--no-locale" -- Avoid issues with impure host system locale in dev mode
-                , "--encoding"
-                , "UTF8"
-            ]
-
-    let params = (Process.proc "postgres" ["-D", "build/db/state", "-k", currentDir <> "/build/db", "-c", "listen_addresses="])
-                { Process.std_in = Process.CreatePipe
-                , Process.std_out = Process.CreatePipe
-                , Process.std_err = Process.CreatePipe
-                }
-
-    Process.withCreateProcess params \(Just inputHandle) (Just outputHandle) (Just errorHandle) processHandle -> do
-        waitUntilReady errorHandle do
-            Process.callProcess "createdb" ["app", "-h", currentDir <> "/build/db"]
-
-            let importSql file = Process.callCommand ("psql -h '" <> currentDir <> "/build/db' -d app < " <> file)
-            ihpSchemaSql <- getDataFileName "IHPSchema.sql"
-            importSql ihpSchemaSql
-            importSql "Application/Schema.sql"
-            importSql "Application/Fixtures.sql"
-
-            Process.terminateProcess processHandle
-            _ <- Process.waitForProcess processHandle
-            pure ()
-
-waitUntilReady handle callback = do
-    line <- ByteString.hGetLine handle
-    if "database system is ready to accept connections" `ByteString.isInfixOf` line
-        then callback
-        else waitUntilReady handle callback
+import qualified System.Process as Process
+import System.Exit (ExitCode(..))
 
 waitPostgres :: (?context :: Context) => IO ()
 waitPostgres = do
     let isDebugMode = ?context.isDebugMode
-    threadDelay 1000000
-    (_, stdout, _) <- Process.readProcessWithExitCode "pg_ctl" ["status"] ""
-    if "server is running" `isInfixOf` (cs stdout)
-    then pure ()
-    else do
-        when isDebugMode (Log.debug ("Waiting for postgres to start" :: Text))
-        waitPostgres
-
-
-withBuiltinOrDevenvPostgres :: (?context :: Context) => (MVar () -> IORef ByteString.Builder -> IORef ByteString.Builder -> IO a) -> IO a
-withBuiltinOrDevenvPostgres callback = do
-    useDevenv <- EnvVar.envOrDefault "IHP_DEVENV" False
-    if useDevenv
-    then do
-        waitPostgres
-
-        -- For devenv postgres we don't have access to the postgres logs
-        standardOutput <- newIORef mempty
-        errorOutput <- newIORef mempty
-        databaseIsReady <- newMVar ()
+    socketDir <- EnvVar.env @String "PGHOST"
 
-        callback databaseIsReady standardOutput errorOutput
-    else do
-        withPostgres callback
+    -- pg_isready returns exit code 0 when ready, non-zero otherwise
+    exitCode <- Process.rawSystem "pg_isready" ["-h", socketDir, "-q"]
+    case exitCode of
+        ExitSuccess -> pure ()
+        ExitFailure _ -> do
+            when isDebugMode (Log.debug ("Waiting for postgres to start" :: Text))
+            threadDelay 100000  -- 100ms between checks
+            waitPostgres
diff --git a/IHP/IDE/Prelude.hs b/IHP/IDE/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/IHP/IDE/Prelude.hs
@@ -0,0 +1,38 @@
+{-# OPTIONS_HADDOCK not-home, hide #-}
+-- | Minimal prelude for IDE controllers
+--
+-- This module provides a focused subset of 'IHP.ControllerPrelude' optimized
+-- for faster compilation of IDE controllers. It includes only the commonly
+-- needed imports for SchemaDesigner and other IDE controllers.
+module IHP.IDE.Prelude
+    ( module IHP.Prelude
+    , module IHP.ControllerSupport
+    , module IHP.Controller.Param
+    , module IHP.Controller.Render
+    , module IHP.Controller.Redirect
+    , module IHP.Controller.Layout
+    , module IHP.FlashMessages
+    , module IHP.Modal.Types
+    , setModal
+    , module IHP.ValidationSupport
+    ) where
+
+import IHP.Prelude
+import IHP.ControllerSupport
+import IHP.Controller.Param
+import IHP.Controller.Render
+import IHP.Controller.Redirect
+import IHP.Controller.Layout
+import IHP.FlashMessages
+import IHP.Modal.Types
+import qualified IHP.Modal.ControllerFunctions as Modal
+import IHP.ViewSupport (View)
+import qualified IHP.ViewSupport as ViewSupport
+import IHP.ValidationSupport
+
+-- | Renders a view and stores it as modal HTML in the context for later rendering.
+--
+-- > setModal MyModalView { .. }
+--
+setModal :: (?context :: ControllerContext, ?request :: Request, View view) => view -> IO ()
+setModal view = let ?view = view in Modal.setModal (ViewSupport.html view)
diff --git a/IHP/IDE/SchemaDesigner/Compiler.hs b/IHP/IDE/SchemaDesigner/Compiler.hs
--- a/IHP/IDE/SchemaDesigner/Compiler.hs
+++ b/IHP/IDE/SchemaDesigner/Compiler.hs
@@ -1,507 +1,19 @@
 {-|
 Module: IHP.IDE.SchemaDesigner.Compiler
-Description: Compiles AST of SQL to DDL
+Description: IHP-specific schema file utilities
 Copyright: (c) digitally induced GmbH, 2020
 -}
-module IHP.IDE.SchemaDesigner.Compiler (compileSql, writeSchema, compileIdentifier, compileExpression, compilePostgresType, compileIndexColumn) where
+module IHP.IDE.SchemaDesigner.Compiler
+( writeSchema
+) where
 
 import IHP.Prelude
-import IHP.IDE.SchemaDesigner.Types
-import Data.Maybe (fromJust)
+import IHP.Postgres.Compiler (compileSql, compareStatement)
+import IHP.Postgres.Types (Statement)
 import qualified Data.Text.IO as Text
-import qualified Data.Text as Text
 
+-- | Write statements to the IHP schema file at 'Application/Schema.sql'
 writeSchema :: [Statement] -> IO ()
 writeSchema !statements = do
     let sortedStatements = sortBy compareStatement statements
     Text.writeFile "Application/Schema.sql" (compileSql sortedStatements)
-
-compileSql :: [Statement] -> Text
-compileSql statements = statements
-    |> map compileStatement
-    |> unlines
-
-compileStatement :: Statement -> Text
-compileStatement (StatementCreateTable CreateTable { name, columns, primaryKeyConstraint, constraints, unlogged }) = "CREATE" <> (if unlogged then " UNLOGGED" else "") <> " TABLE " <> compileIdentifier name <> " (\n" <> intercalate ",\n" (map (\col -> "    " <> compileColumn primaryKeyConstraint col) columns <> maybe [] ((:[]) . indent) (compilePrimaryKeyConstraint primaryKeyConstraint) <> map (indent . compileConstraint) constraints) <> "\n);"
-compileStatement CreateEnumType { name, values } = "CREATE TYPE " <> compileIdentifier name <> " AS ENUM (" <> intercalate ", " (values |> map TextExpression |> map compileExpression) <> ");"
-compileStatement CreateExtension { name, ifNotExists } = "CREATE EXTENSION " <> (if ifNotExists then "IF NOT EXISTS " else "") <> compileIdentifier name <> ";"
-compileStatement AddConstraint { tableName, constraint = UniqueConstraint { name = Nothing, columnNames } } = "ALTER TABLE " <> compileIdentifier tableName <> " ADD UNIQUE (" <> intercalate ", " columnNames <> ")" <> ";"
-compileStatement AddConstraint { tableName, constraint, deferrable, deferrableType } = "ALTER TABLE " <> compileIdentifier tableName <> " ADD CONSTRAINT " <> compileIdentifier (fromMaybe (error "compileStatement: Expected constraint name") (constraint.name)) <> " " <> compileConstraint constraint <> compileDeferrable deferrable deferrableType <> ";"
-compileStatement AddColumn { tableName, column } = "ALTER TABLE " <> compileIdentifier tableName <> " ADD COLUMN " <> (compileColumn (PrimaryKeyConstraint []) column) <> ";"
-compileStatement DropColumn { tableName, columnName } = "ALTER TABLE " <> compileIdentifier tableName <> " DROP COLUMN " <> compileIdentifier columnName <> ";"
-compileStatement RenameColumn { tableName, from, to } = "ALTER TABLE " <> compileIdentifier tableName <> " RENAME COLUMN " <> compileIdentifier from <> " TO " <> compileIdentifier to <> ";"
-compileStatement DropTable { tableName } = "DROP TABLE " <> compileIdentifier tableName <> ";"
-compileStatement Comment { content } = "--" <> content
-compileStatement CreateIndex { indexName, unique, tableName, columns, whereClause, indexType } = "CREATE" <> (if unique then " UNIQUE " else " ") <> "INDEX " <> compileIdentifier indexName <> " ON " <> compileIdentifier tableName <> (maybe "" (\indexType -> " USING " <> compileIndexType indexType) indexType) <> " (" <> (intercalate ", " (map compileIndexColumn columns)) <> ")" <> (case whereClause of Just expression -> " WHERE " <> compileExpression expression; Nothing -> "") <> ";"
-compileStatement CreateFunction { functionName, functionArguments, functionBody, orReplace, returns, language } = "CREATE " <> (if orReplace then "OR REPLACE " else "") <> "FUNCTION " <> functionName <> "(" <> (functionArguments |> map (\(argName, argType) -> argName ++ " " ++ compilePostgresType argType) |> intercalate  ", ") <> ")" <> " RETURNS " <> compilePostgresType returns <> " AS $$" <> functionBody <> "$$ language " <> language <> ";"
-compileStatement EnableRowLevelSecurity { tableName } = "ALTER TABLE " <> compileIdentifier tableName <> " ENABLE ROW LEVEL SECURITY;"
-compileStatement CreatePolicy { name, action, tableName, using, check } = "CREATE POLICY " <> compileIdentifier name <> " ON " <> compileIdentifier tableName <> maybe "" (\action -> " FOR " <> compilePolicyAction action) action  <> maybe "" (\expr -> " USING (" <> compileExpression expr <> ")") using <> maybe "" (\expr -> " WITH CHECK (" <> compileExpression expr <> ")") check <> ";"
-compileStatement CreateSequence { name } = "CREATE SEQUENCE " <> compileIdentifier name <> ";"
-compileStatement DropConstraint { tableName, constraintName } = "ALTER TABLE " <> compileIdentifier tableName <> " DROP CONSTRAINT " <> compileIdentifier constraintName <> ";"
-compileStatement DropEnumType { name } = "DROP TYPE " <> compileIdentifier name <> ";"
-compileStatement DropIndex { indexName } = "DROP INDEX " <> compileIdentifier indexName <> ";"
-compileStatement DropNotNull { tableName, columnName } = "ALTER TABLE " <> compileIdentifier tableName <> " ALTER COLUMN " <> compileIdentifier columnName <> " DROP NOT NULL;"
-compileStatement SetNotNull { tableName, columnName } = "ALTER TABLE " <> compileIdentifier tableName <> " ALTER COLUMN " <> compileIdentifier columnName <> " SET NOT NULL;"
-compileStatement RenameTable { from, to } = "ALTER TABLE " <> compileIdentifier from <> " RENAME TO " <> compileIdentifier to <> ";"
-compileStatement DropPolicy { tableName, policyName } =  "DROP POLICY " <> compileIdentifier policyName <> " ON " <> compileIdentifier tableName <> ";"
-compileStatement SetDefaultValue { tableName, columnName, value } = "ALTER TABLE " <> compileIdentifier tableName <> " ALTER COLUMN " <> compileIdentifier columnName <> " SET DEFAULT " <> compileExpression value <> ";"
-compileStatement DropDefaultValue { tableName, columnName } = "ALTER TABLE " <> compileIdentifier tableName <> " ALTER COLUMN " <> compileIdentifier columnName <> " DROP DEFAULT;"
-compileStatement AddValueToEnumType { enumName, newValue } = "ALTER TYPE " <> compileIdentifier enumName <> " ADD VALUE " <> compileExpression (TextExpression newValue) <> ";"
-compileStatement CreateTrigger { name, eventWhen, event, tableName, for, whenCondition, functionName, arguments } = "CREATE TRIGGER " <> compileIdentifier name <> " " <> compileTriggerEventWhen eventWhen <> " " <> compileTriggerEvent event <> " ON " <> compileIdentifier tableName <> " " <> compileTriggerFor for <> " EXECUTE FUNCTION " <> compileExpression (CallExpression functionName arguments) <> ";"
-compileStatement Begin = "BEGIN;"
-compileStatement Commit = "COMMIT;"
-compileStatement DropFunction { functionName } = "DROP FUNCTION " <> compileIdentifier functionName <> ";"
-compileStatement UnknownStatement { raw } = raw <> ";"
-compileStatement Set { name, value } = "SET " <> compileIdentifier name <> " = " <> compileExpression value <> ";"
-compileStatement SelectStatement { query } = "SELECT " <> query <> ";"
-compileStatement DropTrigger { name, tableName } = "DROP TRIGGER " <> compileIdentifier name <> " ON " <> compileIdentifier tableName <> ";"
-compileStatement CreateEventTrigger { name, eventOn, whenCondition, functionName, arguments } = "CREATE EVENT TRIGGER " <> compileIdentifier name <> " ON " <> compileIdentifier eventOn <> " " <> (maybe "" (\expression -> "WHEN " <> compileExpression expression) whenCondition) <> " EXECUTE FUNCTION " <> compileExpression (CallExpression functionName arguments) <> ";"
-compileStatement DropEventTrigger { name } = "DROP EVENT TRIGGER " <> compileIdentifier name <> ";"
-
--- | Emit a PRIMARY KEY constraint when there are multiple primary key columns
-compilePrimaryKeyConstraint :: PrimaryKeyConstraint -> Maybe Text
-compilePrimaryKeyConstraint PrimaryKeyConstraint { primaryKeyColumnNames } =
-    case primaryKeyColumnNames of
-        [] -> Nothing
-        [_] -> Nothing
-        names -> Just $ "PRIMARY KEY(" <> intercalate ", " names <> ")"
-
-compileConstraint :: Constraint -> Text
-compileConstraint ForeignKeyConstraint { columnName, referenceTable, referenceColumn, onDelete } = "FOREIGN KEY (" <> compileIdentifier columnName <> ") REFERENCES " <> compileIdentifier referenceTable <> (if isJust referenceColumn then " (" <> fromJust referenceColumn <> ")" else "") <> " " <> compileOnDelete onDelete
-compileConstraint UniqueConstraint { columnNames } = "UNIQUE(" <> intercalate ", " columnNames <> ")"
-compileConstraint CheckConstraint { checkExpression } = "CHECK (" <> compileExpression checkExpression <> ")"
-compileConstraint ExcludeConstraint { excludeElements, predicate, indexType } = "EXCLUDE" <> compiledIndexType <> " (" <> compiledExcludeElements <> ")" <> case predicate of
-    Just expression -> " WHERE (" <> compileExpression expression <> ")"
-    Nothing -> ""
-    where
-        compiledExcludeElements = intercalate ", " $ map compileExcludeElement excludeElements
-
-        compileExcludeElement ExcludeConstraintElement { element, operator } = element <> " WITH " <> operator
-
-        compiledIndexType = case indexType of
-            Nothing -> ""
-            Just indexType -> " USING " <> compileIndexType indexType
-
-compileDeferrable :: Maybe Bool -> Maybe DeferrableType -> Text
-compileDeferrable deferrable deferrableType = Text.concat $ map ((<>) " ") $ catMaybes [compileIsDeferrable <$> deferrable, compileDeferrableType <$> deferrableType]
-    where
-        compileIsDeferrable True = "DEFERRABLE"
-        compileIsDeferrable False = "NOT DEFERRABLE"
-        compileDeferrableType InitiallyImmediate = "INITIALLY IMMEDIATE"
-        compileDeferrableType InitiallyDeferred = "INITIALLY DEFERRED"
-
-compileOnDelete :: Maybe OnDelete -> Text
-compileOnDelete Nothing = ""
-compileOnDelete (Just NoAction) = "ON DELETE NO ACTION"
-compileOnDelete (Just Restrict) = "ON DELETE RESTRICT"
-compileOnDelete (Just SetNull) = "ON DELETE SET NULL"
-compileOnDelete (Just SetDefault) = "ON DELETE SET DEFAULT"
-compileOnDelete (Just Cascade) = "ON DELETE CASCADE"
-
-compileColumn :: PrimaryKeyConstraint -> Column -> Text
-compileColumn primaryKeyConstraint Column { name, columnType, defaultValue, notNull, isUnique, generator } =
-    unwords (catMaybes
-        [ Just (compileIdentifier name)
-        , Just (compilePostgresType columnType)
-        , fmap compileDefaultValue defaultValue
-        , fmap compileGenerator generator
-        , primaryKeyColumnConstraint
-        , if notNull then Just "NOT NULL" else Nothing
-        , if isUnique then Just "UNIQUE" else Nothing
-        ])
-    where
-        -- Emit a PRIMARY KEY column constraint if this is the only primary key column
-        primaryKeyColumnConstraint = case primaryKeyConstraint of
-            PrimaryKeyConstraint [primaryKeyColumn]
-                | name == primaryKeyColumn -> Just "PRIMARY KEY"
-                | otherwise -> Nothing
-            PrimaryKeyConstraint _ -> Nothing
-
-compileDefaultValue :: Expression -> Text
-compileDefaultValue value = "DEFAULT " <> compileExpression value
-
-compileExpression :: Expression -> Text
-compileExpression (TextExpression value) = "'" <> value <> "'"
-compileExpression (VarExpression name) =
-        if nameContainsSpaces
-            then compileIdentifier name
-            else name
-    where
-        nameContainsSpaces = Text.any (== ' ') name
-compileExpression (CallExpression func args) = func <> "(" <> intercalate ", " (map compileExpressionWithOptionalParenthese args) <> ")"
-compileExpression (NotEqExpression a b) = compileExpression a <> " <> " <> compileExpression b
-compileExpression (EqExpression a b) = compileExpressionWithOptionalParenthese a <> " = " <> compileExpressionWithOptionalParenthese b
-compileExpression (IsExpression a (NotExpression b)) = compileExpressionWithOptionalParenthese a <> " IS NOT " <> compileExpressionWithOptionalParenthese b -- 'IS (NOT NULL)' => 'IS NOT NULL'
-compileExpression (IsExpression a b) = compileExpressionWithOptionalParenthese a <> " IS " <> compileExpressionWithOptionalParenthese b
-compileExpression (InExpression a b) = compileExpressionWithOptionalParenthese a <> " IN " <> compileExpressionWithOptionalParenthese b
-compileExpression (InArrayExpression values) = "(" <> intercalate ", " (map compileExpression values) <> ")"
-compileExpression (NotExpression a) = "NOT " <> compileExpressionWithOptionalParenthese a
-compileExpression (AndExpression a b) = compileExpressionWithOptionalParenthese a <> " AND " <> compileExpressionWithOptionalParenthese b
-compileExpression (OrExpression a b) = compileExpressionWithOptionalParenthese a <> " OR " <> compileExpressionWithOptionalParenthese b
-compileExpression (LessThanExpression a b) = compileExpressionWithOptionalParenthese a <> " < " <> compileExpressionWithOptionalParenthese b
-compileExpression (LessThanOrEqualToExpression a b) = compileExpressionWithOptionalParenthese a <> " <= " <> compileExpressionWithOptionalParenthese b
-compileExpression (GreaterThanExpression a b) = compileExpressionWithOptionalParenthese a <> " > " <> compileExpressionWithOptionalParenthese b
-compileExpression (GreaterThanOrEqualToExpression a b) = compileExpressionWithOptionalParenthese a <> " >= " <> compileExpressionWithOptionalParenthese b
-compileExpression (DoubleExpression double) = tshow double
-compileExpression (IntExpression integer) = tshow integer
-compileExpression (TypeCastExpression value type_) = compileExpression value <> "::" <> compilePostgresType type_
-compileExpression (SelectExpression Select { columns, from, whereClause }) = "SELECT " <> intercalate ", " (map compileExpression columns) <> " FROM " <> compileExpression from <> " WHERE " <> compileExpression whereClause
-compileExpression (ExistsExpression a) = "EXISTS " <> compileExpressionWithOptionalParenthese a
-compileExpression (DotExpression a b) = compileExpressionWithOptionalParenthese a <> "." <> compileIdentifier b
-compileExpression (ConcatenationExpression a b) = compileExpressionWithOptionalParenthese a <> " || " <> compileExpressionWithOptionalParenthese b
-
-compileExpressionWithOptionalParenthese :: Expression -> Text
-compileExpressionWithOptionalParenthese expr@(VarExpression {}) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(IsExpression a (NotExpression b)) = compileExpression a <> " IS " <> compileExpression (NotExpression b) -- 'IS (NOT NULL)' => 'IS NOT NULL'
-compileExpressionWithOptionalParenthese expr@(IsExpression {}) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(EqExpression {}) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(AndExpression a@(AndExpression {}) b ) = "(" <> compileExpression a <> " AND " <> compileExpressionWithOptionalParenthese b <> ")" -- '(a AND b) AND c' => 'a AND b AND C'
-compileExpressionWithOptionalParenthese expr@(AndExpression a b@(AndExpression {}) ) = "(" <> compileExpressionWithOptionalParenthese a <> " AND " <> compileExpression b <> ")" -- 'a AND (b AND c)' => 'a AND b AND C'
---compileExpressionWithOptionalParenthese expr@(OrExpression a@(IsExpression {}) b ) = compileExpressionWithOptionalParenthese a <> " OR " <> compileExpressionWithOptionalParenthese b -- '(a IS NULL) OR b' => 'A IS NULL OR b'
-compileExpressionWithOptionalParenthese expr@(CallExpression {}) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(TextExpression {}) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(IntExpression {}) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(DoubleExpression {}) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(DotExpression (VarExpression {}) b) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(ConcatenationExpression a b ) = compileExpression expr
-compileExpressionWithOptionalParenthese expr@(InArrayExpression values) = compileExpression expr
-compileExpressionWithOptionalParenthese expression = "(" <> compileExpression expression <> ")"
-
-compareStatement (CreateEnumType {}) _ = LT
-compareStatement (StatementCreateTable CreateTable {}) (AddConstraint {}) = LT
-compareStatement (AddConstraint { constraint = a }) (AddConstraint { constraint = b }) = compare (a.name) (b.name)
-compareStatement (AddConstraint {}) _ = GT
-compareStatement _ _ = EQ
-
-compilePostgresType :: PostgresType -> Text
-compilePostgresType PUUID = "UUID"
-compilePostgresType PText = "TEXT"
-compilePostgresType PInt = "INT"
-compilePostgresType PSmallInt = "SMALLINT"
-compilePostgresType PBigInt = "BIGINT"
-compilePostgresType PBoolean = "BOOLEAN"
-compilePostgresType PTimestamp = "TIMESTAMP WITHOUT TIME ZONE"
-compilePostgresType PTimestampWithTimezone = "TIMESTAMP WITH TIME ZONE"
-compilePostgresType PReal = "REAL"
-compilePostgresType PDouble = "DOUBLE PRECISION"
-compilePostgresType PPoint = "POINT"
-compilePostgresType PPolygon = "POLYGON"
-compilePostgresType PDate = "DATE"
-compilePostgresType PBinary = "BYTEA"
-compilePostgresType PTime = "TIME"
-compilePostgresType (PInterval Nothing) = "INTERVAL"
-compilePostgresType (PInterval (Just fields)) = "INTERVAL" <> " " <> fields
-compilePostgresType (PNumeric (Just precision) (Just scale)) = "NUMERIC(" <> show precision <> "," <> show scale <> ")"
-compilePostgresType (PNumeric (Just precision) Nothing) = "NUMERIC(" <> show precision <> ")"
-compilePostgresType (PNumeric Nothing _) = "NUMERIC"
-compilePostgresType (PVaryingN (Just limit)) = "CHARACTER VARYING(" <> show limit <> ")"
-compilePostgresType (PVaryingN Nothing) = "CHARACTER VARYING"
-compilePostgresType (PCharacterN length) = "CHARACTER(" <> show length <> ")"
-compilePostgresType PSingleChar = "\"char\""
-compilePostgresType PSerial = "SERIAL"
-compilePostgresType PBigserial = "BIGSERIAL"
-compilePostgresType PJSONB = "JSONB"
-compilePostgresType PInet = "INET"
-compilePostgresType PTSVector = "TSVECTOR"
-compilePostgresType (PArray type_) = compilePostgresType type_ <> "[]"
-compilePostgresType PTrigger = "TRIGGER"
-compilePostgresType PEventTrigger = "EVENT_TRIGGER"
-compilePostgresType (PCustomType theType) = theType
-
-compileIdentifier :: Text -> Text
-compileIdentifier identifier = if identifierNeedsQuoting then tshow identifier else identifier
-    where
-        identifierNeedsQuoting = isKeyword || containsChar ' ' || containsChar '-' || isUsingUppercase
-        isKeyword = IHP.Prelude.toUpper identifier `elem` keywords
-        containsChar char = Text.any (char ==) identifier
-        isUsingUppercase = Text.toLower identifier /= identifier
-
-        keywords = [ "ABORT"
-            , "ABSOLUTE"
-            , "ACCESS"
-            , "ACTION"
-            , "ADD"
-            , "ADMIN"
-            , "AFTER"
-            , "AGGREGATE"
-            , "ALSO"
-            , "ALTER"
-            , "ASSERTION"
-            , "ASSIGNMENT"
-            , "AT"
-            , "ALL"
-            , "BACKWARD"
-            , "BEFORE"
-            , "BEGIN"
-            , "BY"
-            , "CACHE"
-            , "CALLED"
-            , "CASCADE"
-            , "CHAIN"
-            , "CHARACTERISTICS"
-            , "CHECKPOINT"
-            , "CLASS"
-            , "CLOSE"
-            , "CLUSTER"
-            , "COMMENT"
-            , "COMMIT"
-            , "COMMITTED"
-            , "CONNECTION"
-            , "CONSTRAINTS"
-            , "CONVERSION"
-            , "COPY"
-            , "CREATEDB"
-            , "CREATEROLE"
-            , "CREATEUSER"
-            , "CSV"
-            , "CURSOR"
-            , "CYCLE"
-            , "DATABASE"
-            , "DAY"
-            , "DEALLOCATE"
-            , "DECLARE"
-            , "DEFAULTS"
-            , "DEFERRED"
-            , "DEFINER"
-            , "DELETE"
-            , "DELIMITER"
-            , "DELIMITERS"
-            , "DISABLE"
-            , "DOMAIN"
-            , "DOUBLE"
-            , "DROP"
-            , "EACH"
-            , "ENABLE"
-            , "ENCODING"
-            , "ENCRYPTED"
-            , "ESCAPE"
-            , "EXCLUDING"
-            , "EXCLUSIVE"
-            , "EXECUTE"
-            , "EXPLAIN"
-            , "EXTERNAL"
-            , "FETCH"
-            , "FIRST"
-            , "FORCE"
-            , "FORWARD"
-            , "FUNCTION"
-            , "GLOBAL"
-            , "GRANTED"
-            , "HANDLER"
-            , "HEADER"
-            , "HOLD"
-            , "HOUR"
-            , "IMMEDIATE"
-            , "IMMUTABLE"
-            , "IMPLICIT"
-            , "INCLUDING"
-            , "INCREMENT"
-            , "INDEX"
-            , "INHERIT"
-            , "INHERITS"
-            , "INPUT"
-            , "INSENSITIVE"
-            , "INSERT"
-            , "INSTEAD"
-            , "INVOKER"
-            , "ISOLATION"
-            , "KEY"
-            , "LANCOMPILER"
-            , "LANGUAGE"
-            , "LARGE"
-            , "LAST"
-            , "LEVEL"
-            , "LISTEN"
-            , "LOAD"
-            , "LOCAL"
-            , "LOCATION"
-            , "LOCK"
-            , "LOGIN"
-            , "MATCH"
-            , "MAXVALUE"
-            , "MINUTE"
-            , "MINVALUE"
-            , "MODE"
-            , "MONTH"
-            , "MOVE"
-            , "NAMES"
-            , "NEXT"
-            , "NO"
-            , "NOCREATEDB"
-            , "NOCREATEROLE"
-            , "NOCREATEUSER"
-            , "NOINHERIT"
-            , "NOLOGIN"
-            , "NOSUPERUSER"
-            , "NOTHING"
-            , "NOTIFY"
-            , "NOWAIT"
-            , "OBJECT"
-            , "OF"
-            , "OIDS"
-            , "OPERATOR"
-            , "OPTION"
-            , "OWNER"
-            , "PARTIAL"
-            , "PASSWORD"
-            , "PREPARE"
-            , "PREPARED"
-            , "PRESERVE"
-            , "PRIOR"
-            , "PRIVILEGES"
-            , "PROCEDURAL"
-            , "PROCEDURE"
-            , "QUOTE"
-            , "READ"
-            , "RECHECK"
-            , "REINDEX"
-            , "RELATIVE"
-            , "RELEASE"
-            , "RENAME"
-            , "REPEATABLE"
-            , "REPLACE"
-            , "RESET"
-            , "RESTART"
-            , "RESTRICT"
-            , "RETURNS"
-            , "REVOKE"
-            , "ROLE"
-            , "ROLLBACK"
-            , "ROWS"
-            , "RULE"
-            , "SAVEPOINT"
-            , "SCHEMA"
-            , "SCROLL"
-            , "SECOND"
-            , "SECURITY"
-            , "SEQUENCE"
-            , "SERIALIZABLE"
-            , "SESSION"
-            , "SET"
-            , "SHARE"
-            , "tshow"
-            , "SIMPLE"
-            , "STABLE"
-            , "START"
-            , "STATEMENT"
-            , "STATISTICS"
-            , "STDIN"
-            , "STDOUT"
-            , "STORAGE"
-            , "STRICT"
-            , "SUPERUSER"
-            , "SYSID"
-            , "SYSTEM"
-            , "TABLESPACE"
-            , "TEMP"
-            , "TEMPLATE"
-            , "TEMPORARY"
-            , "TOAST"
-            , "TRANSACTION"
-            , "TRIGGER"
-            , "TRUNCATE"
-            , "TRUSTED"
-            , "TYPE"
-            , "UNCOMMITTED"
-            , "UNENCRYPTED"
-            , "UNKNOWN"
-            , "UNLISTEN"
-            , "UNTIL"
-            , "UPDATE"
-            , "VACUUM"
-            , "VALID"
-            , "VALIDATOR"
-            , "VALUES"
-            , "VARYING"
-            , "VIEW"
-            , "VOLATILE"
-            , "WITH"
-            , "WITHOUT"
-            , "WORK"
-            , "WRITE"
-            , "YEAR"
-            , "ZONE"
-            , "BIGINT"
-            , "BIT"
-            , "BOOLEAN"
-            , "CHAR"
-            , "CHARACTER"
-            , "COALESCE"
-            , "CONVERT"
-            , "DEC"
-            , "DECIMAL"
-            , "EXISTS"
-            , "EXTRACT"
-            , "FLOAT"
-            , "GREATEST"
-            , "INOUT"
-            , "INT"
-            , "INTEGER"
-            , "INTERVAL"
-            , "LEAST"
-            , "NATIONAL"
-            , "NCHAR"
-            , "NONE"
-            , "NULLIF"
-            , "NUMERIC"
-            , "OUT"
-            , "OVERLAY"
-            , "POSITION"
-            , "PRECISION"
-            , "REAL"
-            , "ROW"
-            , "SETOF"
-            , "SMALLINT"
-            , "SUBSTRING"
-            , "TIME"
-            , "TIMESTAMP"
-            , "TREAT"
-            , "TRIM"
-            , "VARCHAR"
-            ]
-
-indent text = "    " <> text
-
-compileTriggerEventWhen :: TriggerEventWhen -> Text
-compileTriggerEventWhen Before = "BEFORE"
-compileTriggerEventWhen After = "AFTER"
-compileTriggerEventWhen InsteadOf = "INSTEAD OF"
-
-compileTriggerEvent :: TriggerEvent -> Text
-compileTriggerEvent TriggerOnInsert = "INSERT"
-compileTriggerEvent TriggerOnUpdate = "UPDATE"
-compileTriggerEvent TriggerOnDelete = "DELETE"
-compileTriggerEvent TriggerOnTruncate = "TRUNCATE"
-
-compileTriggerFor :: TriggerFor -> Text
-compileTriggerFor ForEachRow = "FOR EACH ROW"
-compileTriggerFor ForEachStatement = "FOR EACH STATEMENT"
-
-compilePolicyAction :: PolicyAction -> Text
-compilePolicyAction PolicyForAll = "ALL"
-compilePolicyAction PolicyForSelect = "SELECT"
-compilePolicyAction PolicyForInsert = "INSERT"
-compilePolicyAction PolicyForUpdate = "UPDATE"
-compilePolicyAction PolicyForDelete = "DELETE"
-
-compileGenerator :: ColumnGenerator -> Text
-compileGenerator ColumnGenerator { generate, stored } =
-    "GENERATED ALWAYS AS ("
-    <> compileExpressionWithOptionalParenthese generate
-    <> ")"
-    <> (if stored then " STORED" else "")
-
-compileIndexType :: IndexType -> Text
-compileIndexType Gin = "GIN"
-compileIndexType Btree = "BTREE"
-compileIndexType Gist = "GIST"
-
-compileIndexColumn :: IndexColumn -> Text
-compileIndexColumn IndexColumn { column, columnOrder = [] } = compileExpression column
-compileIndexColumn IndexColumn { column, columnOrder } = compileExpression column <> " " <> unwords (columnOrder |> map compileIndexColumnOrder)
-
-compileIndexColumnOrder :: IndexColumnOrder -> Text
-compileIndexColumnOrder Asc = "ASC"
-compileIndexColumnOrder Desc = "DESC"
-compileIndexColumnOrder NullsFirst = "NULLS FIRST"
-compileIndexColumnOrder NullsLast = "NULLS LAST"
diff --git a/IHP/IDE/SchemaDesigner/Controller/Columns.hs b/IHP/IDE/SchemaDesigner/Controller/Columns.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Columns.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Columns.hs
@@ -1,6 +1,6 @@
 module IHP.IDE.SchemaDesigner.Controller.Columns where
 
-import IHP.ControllerPrelude
+import IHP.IDE.Prelude
 import IHP.IDE.ToolServer.Types
 
 import IHP.IDE.SchemaDesigner.View.Columns.New
@@ -8,7 +8,7 @@
 import IHP.IDE.SchemaDesigner.View.Columns.NewForeignKey
 import IHP.IDE.SchemaDesigner.View.Columns.EditForeignKey
 
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.SchemaDesigner.View.Layout (schemaDesignerLayout, findStatementByName, replace)
 import IHP.IDE.SchemaDesigner.Controller.Helper
 import IHP.IDE.SchemaDesigner.Controller.Validation
@@ -33,6 +33,8 @@
         case validationResult of
             Failure message ->
                 setErrorMessage message
+            FailureHtml message ->
+                setErrorMessage message
             Success -> do
                 let options = SchemaOperations.AddColumnOptions
                         { tableName
@@ -71,6 +73,8 @@
 
         case validationResult of
             Failure message ->
+                setErrorMessage message
+            FailureHtml message ->
                 setErrorMessage message
             Success -> do
                 let options = SchemaOperations.UpdateColumnOptions
diff --git a/IHP/IDE/SchemaDesigner/Controller/EnumValues.hs b/IHP/IDE/SchemaDesigner/Controller/EnumValues.hs
--- a/IHP/IDE/SchemaDesigner/Controller/EnumValues.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/EnumValues.hs
@@ -1,12 +1,12 @@
 module IHP.IDE.SchemaDesigner.Controller.EnumValues where
 
-import IHP.ControllerPrelude
+import IHP.IDE.Prelude
 import IHP.IDE.ToolServer.Types
 
 import IHP.IDE.SchemaDesigner.View.EnumValues.New
 import IHP.IDE.SchemaDesigner.View.EnumValues.Edit
 
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.SchemaDesigner.View.Layout (findStatementByName, replace, schemaDesignerLayout)
 import IHP.IDE.SchemaDesigner.Controller.Helper
 import IHP.IDE.SchemaDesigner.Controller.Validation
@@ -28,6 +28,8 @@
         case validationResult of
             Failure message ->
                 setErrorMessage message
+            FailureHtml message ->
+                setErrorMessage message
             Success -> do
                 updateSchema $ SchemaOperations.addValueToEnum enumName enumValueName
 
@@ -37,8 +39,8 @@
         -- 2. Save & Add another
         --
         case paramOrDefault @Text "Save" "submit" of
-            "Save" -> redirectTo ShowEnumAction { .. }
             "Save & Add Another" -> redirectTo NewEnumValueAction { .. }
+            _ -> redirectTo ShowEnumAction { .. }
 
     action EditEnumValueAction { .. } = do
         statements <- readSchema
@@ -60,6 +62,8 @@
         let validationResult = newValue |> validateEnumValue statements (Just value)
         case validationResult of
             Failure message ->
+                setErrorMessage message
+            FailureHtml message ->
                 setErrorMessage message
             Success ->
                 updateSchema (map (updateValueInEnum enumName newValue valueId))
diff --git a/IHP/IDE/SchemaDesigner/Controller/Enums.hs b/IHP/IDE/SchemaDesigner/Controller/Enums.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Enums.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Enums.hs
@@ -1,13 +1,13 @@
 module IHP.IDE.SchemaDesigner.Controller.Enums where
 
-import IHP.ControllerPrelude
+import IHP.IDE.Prelude
 import IHP.IDE.ToolServer.Types
 
 import IHP.IDE.SchemaDesigner.View.Enums.New
 import IHP.IDE.SchemaDesigner.View.Enums.Show
 import IHP.IDE.SchemaDesigner.View.Enums.Edit
 
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.SchemaDesigner.View.Layout (replace, schemaDesignerLayout)
 import IHP.IDE.SchemaDesigner.Controller.Helper
 import IHP.IDE.SchemaDesigner.Controller.Validation
@@ -34,6 +34,9 @@
             Failure message -> do
                 setErrorMessage message
                 redirectTo TablesAction
+            FailureHtml message -> do
+                setErrorMessage message
+                redirectTo TablesAction
             Success -> do
                 updateSchema $ SchemaOperations.addEnum enumName
                 redirectTo ShowEnumAction { .. }
@@ -51,6 +54,9 @@
         let validationResult = enumName |> validateEnum statements (Just oldEnumName)
         case validationResult of
             Failure message -> do
+                setErrorMessage message
+                redirectTo ShowEnumAction { enumName = oldEnumName }
+            FailureHtml message -> do
                 setErrorMessage message
                 redirectTo ShowEnumAction { enumName = oldEnumName }
             Success -> do
diff --git a/IHP/IDE/SchemaDesigner/Controller/Helper.hs b/IHP/IDE/SchemaDesigner/Controller/Helper.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Helper.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Helper.hs
@@ -1,8 +1,9 @@
 module IHP.IDE.SchemaDesigner.Controller.Helper where
 
-import IHP.ControllerPrelude
-import IHP.IDE.SchemaDesigner.Types
-import qualified IHP.IDE.SchemaDesigner.Parser as Parser
+import IHP.IDE.Prelude
+import IHP.Postgres.Types
+import qualified IHP.Postgres.Parser as Parser
+import qualified IHP.SchemaCompiler.Parser as SchemaDesignerParser
 import qualified Text.Megaparsec as Megaparsec
 import qualified IHP.IDE.SchemaDesigner.Compiler as SchemaCompiler
 import IHP.IDE.SchemaDesigner.View.Schema.Error
@@ -27,13 +28,15 @@
     ( ?context::ControllerContext
     , ?modelContext::ModelContext
     , ?theAction::controller
+    , ?respond::Respond
+    , ?request :: Request
     ) => IO [Statement]
-readSchema = Parser.parseSchemaSql >>= \case
+readSchema = SchemaDesignerParser.parseSchemaSql >>= \case
         Left error -> do render ErrorView { error }; pure []
         Right statements -> pure statements
 
 getSqlError :: IO (Maybe ByteString)
-getSqlError = Parser.parseSchemaSql >>= \case
+getSqlError = SchemaDesignerParser.parseSchemaSql >>= \case
         Left error -> do pure (Just error)
         Right statements -> do pure Nothing
 
@@ -41,6 +44,8 @@
     ( ?context :: ControllerContext
     , ?modelContext::ModelContext
     , ?theAction::controller
+    , ?respond::Respond
+    , ?request :: Request
     ) => ([Statement] -> [Statement]) -> IO ()
 updateSchema updateFn = do
     statements <- readSchema
diff --git a/IHP/IDE/SchemaDesigner/Controller/Indexes.hs b/IHP/IDE/SchemaDesigner/Controller/Indexes.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Indexes.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Indexes.hs
@@ -1,6 +1,6 @@
 module IHP.IDE.SchemaDesigner.Controller.Indexes where
 
-import IHP.ControllerPrelude
+import IHP.IDE.Prelude
 import IHP.IDE.ToolServer.Types
 
 import IHP.IDE.SchemaDesigner.View.Indexes.Edit
diff --git a/IHP/IDE/SchemaDesigner/Controller/Migrations.hs b/IHP/IDE/SchemaDesigner/Controller/Migrations.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Migrations.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Migrations.hs
@@ -15,18 +15,25 @@
 import qualified IHP.IDE.CodeGen.MigrationGenerator as MigrationGenerator
 import IHP.IDE.CodeGen.Controller
 import IHP.IDE.ToolServer.Helper.Controller (openEditor, clearDatabaseNeedsMigration)
-import IHP.Log.Types
 import qualified Control.Exception.Safe as Exception
-import qualified System.Directory as Directory
-import qualified Database.PostgreSQL.Simple as PG
+import qualified System.Directory.OsPath as Directory
+import System.OsPath (encodeUtf)
+import qualified Hasql.Connection as Connection
+import qualified Hasql.Connection.Settings as ConnectionSettings
 
 instance Controller MigrationsController where
     beforeAction = setLayout schemaDesignerLayout
 
     action MigrationsAction = do
         migrations <- findRecentMigrations
-        migratedRevisions <- findMigratedRevisions
 
+        result <- Exception.try findMigratedRevisions
+        migratedRevisions <- case result of
+            Left (exception :: SomeException) -> do
+                setErrorMessage ("Could not connect to the database: " <> tshow exception)
+                pure []
+            Right revisions -> pure revisions
+
         migrationsWithSql <- forM migrations $ \migration -> do
                 sql <- readSqlStatements migration
                 pure (migration, sql)
@@ -59,7 +66,7 @@
                 case result of
                     Left (exception :: SomeException) -> do
                         let errorMessage = case fromException exception of
-                                Just (exception :: EnhancedSqlError) -> cs exception.sqlError.sqlErrorMsg
+                                Just (exception :: EnhancedSqlError) -> enhancedSqlErrorMessage exception
                                 Nothing -> tshow exception
 
                         setErrorMessage errorMessage
@@ -86,9 +93,10 @@
 
     action DeleteMigrationAction { migrationId } = do
         migration <- findMigrationByRevision migrationId
-        path <- cs <$> SchemaMigration.migrationPath migration
+        path <- SchemaMigration.migrationPath migration
+        osPath <- encodeUtf (cs path)
 
-        Directory.removeFile path
+        Directory.removeFile osPath
 
         redirectTo MigrationsAction
 
@@ -99,7 +107,7 @@
         case result of
             Left (exception :: SomeException) -> do
                 let errorMessage = case fromException exception of
-                        Just (exception :: EnhancedSqlError) -> cs exception.sqlError.sqlErrorMsg
+                        Just (exception :: EnhancedSqlError) -> enhancedSqlErrorMessage exception
                         Nothing -> tshow exception
 
                 setErrorMessage errorMessage
@@ -123,37 +131,27 @@
     pure migration
 
 migrateAppDB :: Int -> IO ()
-migrateAppDB revision = withAppModelContext do
+migrateAppDB revision = withMigrateConnection \connection -> do
     let minimumRevision = Just (revision - 1)
-    SchemaMigration.migrate SchemaMigration.MigrateOptions { minimumRevision }
+    SchemaMigration.migrate connection SchemaMigration.MigrateOptions { minimumRevision }
 
 findMigratedRevisions :: IO [Int]
-findMigratedRevisions = emptyListIfTablesDoesntExists (withAppModelContext SchemaMigration.findMigratedRevisions)
+findMigratedRevisions = emptyListIfTablesDoesntExists (withMigrateConnection SchemaMigration.findMigratedRevisions)
     where
         -- The schema_migrations table might not have been created yet
         -- In that case there cannot be any migrations that have been run yet
         emptyListIfTablesDoesntExists operation = do
             result <- Exception.try operation
             case result of
-                Left (EnhancedSqlError { sqlError }) | sqlError.sqlErrorMsg == "relation \"schema_migrations\" does not exist" -> pure []
+                Left (exception :: SomeException)
+                    | "schema_migrations" `isInfixOf` tshow exception -> pure []
+                    | otherwise -> Exception.throwIO exception
                 Right result -> pure result
 
-withAppModelContext :: ((?modelContext :: ModelContext) => IO result) -> IO result
-withAppModelContext inner =
-        Exception.bracket initModelContext cleanupModelContext callback
+withMigrateConnection :: (Connection.Connection -> IO result) -> IO result
+withMigrateConnection inner = Exception.bracket acquire Connection.release inner
     where
-        callback (frameworkConfig, logger, modelContext) = let ?modelContext = modelContext in inner
-        initModelContext = do
+        acquire = do
             frameworkConfig <- buildFrameworkConfig (pure ())
-            logger <- defaultLogger
-
-            modelContext <- createModelContext
-                (frameworkConfig.dbPoolIdleTime)
-                (frameworkConfig.dbPoolMaxConnections)
-                (frameworkConfig.databaseUrl)
-                logger
-
-            pure (frameworkConfig, logger, modelContext)
-
-        cleanupModelContext (frameworkConfig, logger, modelContext) = do
-            logger |> cleanup
+            Connection.acquire (ConnectionSettings.connectionString (cs frameworkConfig.databaseUrl))
+                >>= either (\e -> error ("DB connect failed: " <> show e)) pure
diff --git a/IHP/IDE/SchemaDesigner/Controller/Policies.hs b/IHP/IDE/SchemaDesigner/Controller/Policies.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Policies.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Policies.hs
@@ -1,12 +1,12 @@
 module IHP.IDE.SchemaDesigner.Controller.Policies where
 
-import IHP.ControllerPrelude
+import IHP.IDE.Prelude
 import IHP.IDE.ToolServer.Types
 
 import IHP.IDE.SchemaDesigner.View.Policies.New
 import IHP.IDE.SchemaDesigner.View.Policies.Edit
 
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.SchemaDesigner.View.Layout (schemaDesignerLayout, findStatementByName)
 import IHP.IDE.SchemaDesigner.Controller.Helper
 
diff --git a/IHP/IDE/SchemaDesigner/Controller/Schema.hs b/IHP/IDE/SchemaDesigner/Controller/Schema.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Schema.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Schema.hs
@@ -1,13 +1,13 @@
 module IHP.IDE.SchemaDesigner.Controller.Schema where
 
-import IHP.ControllerPrelude
+import IHP.IDE.Prelude
 import IHP.IDE.ToolServer.Types
 
 import IHP.IDE.SchemaDesigner.View.Schema.Code
 import IHP.IDE.SchemaDesigner.View.Schema.GeneratedCode
 import IHP.IDE.SchemaDesigner.View.Schema.SchemaUpdateFailed
 
-import IHP.IDE.SchemaDesigner.Parser
+import IHP.SchemaCompiler.Parser
 import qualified IHP.SchemaCompiler as SchemaCompiler
 import qualified System.Process as Process
 import System.Exit
@@ -17,18 +17,21 @@
 import IHP.IDE.SchemaDesigner.View.Layout
 import IHP.IDE.ToolServer.Routes ()
 import IHP.IDE.SchemaDesigner.Controller.Helper
+import System.OsPath (decodeUtf)
 
 instance Controller SchemaController where
     beforeAction = setLayout schemaDesignerLayout
 
     action ShowCodeAction = do
-        schema <- Text.readFile schemaFilePath
+        schemaFP <- decodeUtf schemaFilePath
+        schema <- Text.readFile schemaFP
         error <- getSqlError
         render CodeView { .. }
 
     action SaveCodeAction = do
+        schemaFP <- decodeUtf schemaFilePath
         let schema = param "schemaSql"
-        Text.writeFile schemaFilePath schema
+        Text.writeFile schemaFP schema
         redirectTo ShowCodeAction
 
     action PushToDbAction = do
diff --git a/IHP/IDE/SchemaDesigner/Controller/Tables.hs b/IHP/IDE/SchemaDesigner/Controller/Tables.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Tables.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Tables.hs
@@ -1,6 +1,6 @@
 module IHP.IDE.SchemaDesigner.Controller.Tables where
 
-import IHP.ControllerPrelude
+import IHP.IDE.Prelude
 import IHP.IDE.ToolServer.Types
 
 import IHP.IDE.SchemaDesigner.View.Tables.New
@@ -8,13 +8,14 @@
 import IHP.IDE.SchemaDesigner.View.Tables.Index
 import IHP.IDE.SchemaDesigner.View.Tables.Edit
 
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.SchemaDesigner.View.Layout (findStatementByName, schemaDesignerLayout)
 import qualified IHP.SchemaCompiler as SchemaCompiler
 import IHP.IDE.SchemaDesigner.Controller.Helper
 import IHP.IDE.SchemaDesigner.Controller.Validation
 import IHP.IDE.SchemaDesigner.Controller.Columns (updateForeignKeyConstraint)
 import qualified IHP.IDE.SchemaDesigner.SchemaOperations as SchemaOperations
+import IHP.IDE.CodeGen.Types (defaultUuidFunction)
 
 instance Controller TablesController where
     beforeAction = setLayout schemaDesignerLayout
@@ -49,8 +50,12 @@
             Failure message -> do
                 setErrorMessage message
                 redirectTo TablesAction
+            FailureHtml message -> do
+                setErrorMessage message
+                redirectTo TablesAction
             Success -> do
-                updateSchema (SchemaOperations.addTable tableName)
+                uuidFunction <- defaultUuidFunction
+                updateSchema (SchemaOperations.addTable tableName uuidFunction)
                 redirectTo ShowTableAction { .. }
 
     action EditTableAction { .. } = do
@@ -67,6 +72,9 @@
         let validationResult = tableName |> validateTable statements (Just oldTableName)
         case validationResult of
             Failure message -> do
+                setErrorMessage message
+                redirectTo ShowTableAction { tableName = oldTableName }
+            FailureHtml message -> do
                 setErrorMessage message
                 redirectTo ShowTableAction { tableName = oldTableName }
             Success -> do
diff --git a/IHP/IDE/SchemaDesigner/Controller/Validation.hs b/IHP/IDE/SchemaDesigner/Controller/Validation.hs
--- a/IHP/IDE/SchemaDesigner/Controller/Validation.hs
+++ b/IHP/IDE/SchemaDesigner/Controller/Validation.hs
@@ -1,6 +1,6 @@
 module IHP.IDE.SchemaDesigner.Controller.Validation where
 
-import IHP.ControllerPrelude
+import IHP.IDE.Prelude
 import Text.Countable (singularize)
 
 isUniqueInList :: (Foldable t, Eq a) => t a -> Maybe a -> Validator a
diff --git a/IHP/IDE/SchemaDesigner/Parser.hs b/IHP/IDE/SchemaDesigner/Parser.hs
deleted file mode 100644
--- a/IHP/IDE/SchemaDesigner/Parser.hs
+++ /dev/null
@@ -1,1008 +0,0 @@
-{-|
-Module: IHP.IDE.SchemaDesigner.Types
-Description: Parser for Application/Schema.sql
-Copyright: (c) digitally induced GmbH, 2020
--}
-module IHP.IDE.SchemaDesigner.Parser
-( parseSchemaSql
-, parseSqlFile
-, schemaFilePath
-, parseDDL
-, expression
-, sqlType
-, removeTypeCasts
-, parseIndexColumns
-) where
-
-import IHP.Prelude
-import IHP.IDE.SchemaDesigner.Types
-import qualified Prelude
-import qualified Data.Text.IO as Text
-import Text.Megaparsec
-import Data.Void
-import Text.Megaparsec.Char
-import qualified Text.Megaparsec.Char.Lexer as Lexer
-import Data.Char
-import Control.Monad.Combinators.Expr
-import Data.Functor
-
-schemaFilePath = "Application/Schema.sql"
-
-parseSchemaSql :: IO (Either ByteString [Statement])
-parseSchemaSql = parseSqlFile schemaFilePath
-
-parseSqlFile :: FilePath -> IO (Either ByteString [Statement])
-parseSqlFile schemaFilePath = do
-    schemaSql <- Text.readFile schemaFilePath
-    let result = runParser parseDDL (cs schemaFilePath) schemaSql
-    case result of
-        Left error -> pure (Left (cs $ errorBundlePretty error))
-        Right r -> pure (Right r)
-
-type Parser = Parsec Void Text
-
-spaceConsumer :: Parser ()
-spaceConsumer = Lexer.space
-    space1
-    (Lexer.skipLineComment "//")
-    (Lexer.skipBlockComment "/*" "*/")
-
-lexeme :: Parser a -> Parser a
-lexeme = Lexer.lexeme spaceConsumer
-
-symbol :: Text -> Parser Text
-symbol = Lexer.symbol spaceConsumer
-
-symbol' :: Text -> Parser Text
-symbol' = Lexer.symbol' spaceConsumer
-
-stringLiteral :: Parser String
-stringLiteral = char '\'' *> manyTill Lexer.charLiteral (char '\'')
-
-parseDDL :: Parser [Statement]
-parseDDL = optional space >> (manyTill statement eof)
-
-statement = do
-    space
-    let create = try createExtension <|> try (StatementCreateTable <$> createTable) <|> try createIndex <|> try createFunction <|> try createTrigger <|> try createEnumType <|> try createPolicy <|> try createSequence
-    let alter = do
-            lexeme "ALTER"
-            alterTable <|> alterType <|> alterSequence
-    s <- setStatement <|> create <|> alter <|> selectStatement <|> try dropTable <|> try dropIndex <|> try dropPolicy <|> try dropFunction <|> try dropType <|> dropTrigger <|> commentStatement <|> comment <|> begin <|> commit <|> restrict <|> unrestrict
-    space
-    pure s
-
-
-createExtension = do
-    lexeme "CREATE"
-    lexeme "EXTENSION"
-    ifNotExists <- isJust <$> optional (lexeme "IF" >> lexeme "NOT" >> lexeme "EXISTS")
-    name <- qualifiedIdentifier
-    optional do
-        space
-        lexeme "WITH"
-        lexeme "SCHEMA"
-        lexeme "public"
-    char ';'
-    pure CreateExtension { name, ifNotExists = True }
-
-createTable = do
-    lexeme "CREATE"
-    unlogged <- isJust <$> optional (lexeme "UNLOGGED")
-    lexeme "TABLE"
-    name <- qualifiedIdentifier
-
-    -- Process columns (tagged if they're primary key) and table constraints
-    -- together, as they can be in any order
-    (taggedColumns, allConstraints) <- between (char '(' >> space) (char ')' >> space) do
-        columnsAndConstraints <- ((Right <$> parseTableConstraint) <|> (Left <$> parseColumn)) `sepBy` (char ',' >> space)
-        pure (lefts columnsAndConstraints, rights columnsAndConstraints)
-
-    char ';'
-
-    -- Check that either there is a single column with a PRIMARY KEY constraint,
-    -- or there is a single PRIMARY KEY table constraint
-    let
-        columns = map snd taggedColumns
-        constraints = rights allConstraints
-
-    primaryKeyConstraint <- case filter fst taggedColumns of
-        [] -> case lefts allConstraints of
-            [] -> pure $ PrimaryKeyConstraint []
-            [primaryKeyConstraint] -> pure primaryKeyConstraint
-            _ -> Prelude.fail ("Multiple PRIMARY KEY constraints on table " <> cs name)
-        [(_, Column { name })] -> case lefts allConstraints of
-            [] -> pure $ PrimaryKeyConstraint [name]
-            _ -> Prelude.fail ("Primary key defined in both column and table constraints on table " <> cs name)
-        _ -> Prelude.fail "Multiple columns with PRIMARY KEY constraint"
-
-    pure CreateTable { name, columns, primaryKeyConstraint, constraints, unlogged }
-
-createEnumType = do
-    lexeme "CREATE"
-    lexeme "TYPE"
-    optional do
-        lexeme "public"
-        char '.'
-    name <- identifier
-    lexeme "AS"
-    lexeme "ENUM"
-    values <- between (char '(' >> space) (space >> char ')' >> space) (textExpr' `sepBy` (char ',' >> space))
-    space
-    char ';'
-    pure CreateEnumType { name, values }
-
-addConstraint tableName = do
-    constraint <- parseTableConstraint >>= \case
-      Left primaryKeyConstraint -> pure AlterTableAddPrimaryKey { name = Nothing, primaryKeyConstraint }
-      Right constraint -> pure constraint
-    deferrable <- optional parseDeferrable
-    deferrableType <- optional parseDeferrableType
-    char ';'
-    pure AddConstraint { tableName, constraint, deferrable, deferrableType }
-
-parseDeferrable = do
-    isDeferrable <- lexeme "DEFERRABLE" <|> lexeme "NOT DEFERRABLE"
-    pure $ case isDeferrable of
-        "DEFERRABLE" -> True
-        "NOT DEFERRABLE" -> False
-
-parseDeferrableType = do
-    lexeme "INITIALLY"
-    dtype <- lexeme "IMMEDIATE" <|> lexeme "DEFERRED"
-    case dtype of
-        "IMMEDIATE" -> pure InitiallyImmediate
-        "DEFERRED" -> pure InitiallyDeferred
-
-parseTableConstraint = do
-    name <- optional do
-        lexeme "CONSTRAINT"
-        identifier
-    (Left <$> parsePrimaryKeyConstraint) <|>
-      (Right <$> (parseForeignKeyConstraint name <|> parseUniqueConstraint name <|> parseCheckConstraint name <|> parseExcludeConstraint name))
-
-parsePrimaryKeyConstraint = do
-    lexeme "PRIMARY"
-    lexeme "KEY"
-    primaryKeyColumnNames <- between (char '(' >> space) (char ')' >> space) (identifier `sepBy1` (char ',' >> space))
-    pure PrimaryKeyConstraint { primaryKeyColumnNames }
-
-parseForeignKeyConstraint name = do
-    lexeme "FOREIGN"
-    lexeme "KEY"
-    columnName <- between (char '(' >> space) (char ')' >> space) identifier
-    lexeme "REFERENCES"
-    referenceTable <- qualifiedIdentifier
-    referenceColumn <- optional $ between (char '(' >> space) (char ')' >> space) identifier
-    onDelete <- optional do
-        lexeme "ON"
-        lexeme "DELETE"
-        parseOnDelete
-    pure ForeignKeyConstraint { name, columnName, referenceTable, referenceColumn, onDelete }
-
-parseUniqueConstraint name = do
-    lexeme "UNIQUE"
-    columnNames <- between (char '(' >> space) (char ')' >> space) (identifier `sepBy1` (char ',' >> space))
-    pure UniqueConstraint { name, columnNames }
-
-parseCheckConstraint name = do
-    lexeme "CHECK"
-    checkExpression <- between (char '(' >> space) (char ')' >> space) expression
-    pure CheckConstraint { name, checkExpression }
-
-parseExcludeConstraint name = do
-    lexeme "EXCLUDE"
-    indexType <- optional parseIndexType
-    excludeElements <- between (char '(' >> space) (char ')' >> space) $ excludeElement `sepBy` (char ',' >> space)
-    predicate <- optional do
-        lexeme "WHERE"
-        between (char '(' >> space) (char ')' >> space) expression
-    pure ExcludeConstraint { name, excludeElements, predicate, indexType }
-    where
-        excludeElement = do
-            element <- identifier
-            space
-            lexeme "WITH"
-            space
-            operator <- parseCommutativeInfixOperator
-            pure ExcludeConstraintElement { element, operator }
-
-        parseCommutativeInfixOperator = choice $ map lexeme
-            [ "="
-            , "<>"
-            , "!="
-            , "AND"
-            , "OR"
-            ]
-
-parseOnDelete = choice
-        [ (lexeme "NO" >> lexeme "ACTION") >> pure NoAction
-        , (lexeme "RESTRICT" >> pure Restrict)
-        , (lexeme "SET" >> ((lexeme "NULL" >> pure SetNull) <|> (lexeme "DEFAULT" >> pure SetDefault)))
-        , (lexeme "CASCADE" >> pure Cascade)
-        ]
-
-parseColumn :: Parser (Bool, Column)
-parseColumn = do
-    name <- identifier
-    columnType <- sqlType
-    space
-    defaultValue <- optional do
-        lexeme "DEFAULT"
-        expression
-    generator <- optional do
-        lexeme "GENERATED"
-        lexeme "ALWAYS"
-        lexeme "AS"
-        generate <- expression
-        stored <- isJust <$> optional (lexeme "STORED")
-        pure ColumnGenerator { generate, stored }
-    primaryKey <- isJust <$> optional (lexeme "PRIMARY" >> lexeme "KEY")
-    notNull <- isJust <$> optional (lexeme "NOT" >> lexeme "NULL")
-    isUnique <- isJust <$> optional (lexeme "UNIQUE")
-    pure (primaryKey, Column { name, columnType, defaultValue, notNull, isUnique, generator })
-
-sqlType :: Parser PostgresType
-sqlType = choice $ map optionalArray
-        [ uuid
-        , text
-        , interval --Needs higher precedence otherwise parsed as int
-        , bigint
-        , smallint
-        , int   -- order int after smallint/bigint because symbol INT is prefix of INT2, INT8
-        , bool
-        , timestamp
-        , timestampZ
-        , timestampZ'
-        , timestamp'
-        , real
-        , double
-        , point
-        , polygon
-        , date
-        , binary
-        , time
-        , numericPS
-        , numeric
-        , character
-        , varchar
-        , serial
-        , bigserial
-        , jsonb
-        , inet
-        , tsvector
-        , trigger
-        , eventTrigger
-        , singleChar
-        , customType
-        ]
-            where
-                timestamp = do
-                    try (symbol' "TIMESTAMP" >> symbol' "WITHOUT" >> symbol' "TIME" >> symbol' "ZONE")
-                    pure PTimestamp
-
-                timestampZ = do
-                    try (symbol' "TIMESTAMP" >> symbol' "WITH" >> symbol' "TIME" >> symbol' "ZONE")
-                    pure PTimestampWithTimezone
-
-                timestampZ' = do
-                    try (symbol' "TIMESTAMPZ")
-                    pure PTimestampWithTimezone
-
-                timestamp' = do
-                    try (symbol' "TIMESTAMP")
-                    pure PTimestamp
-
-                uuid = do
-                    try (symbol' "UUID")
-                    pure PUUID
-
-                text = do
-                    try (symbol' "TEXT")
-                    pure PText
-
-                bigint = do
-                    try (symbol' "BIGINT") <|> try (symbol' "INT8")
-                    pure PBigInt
-
-                smallint = do
-                    try (symbol' "SMALLINT") <|> try (symbol' "INT2")
-                    pure PSmallInt
-
-                int = do
-                    try (symbol' "INTEGER") <|> try (symbol' "INT4") <|> try (symbol' "INT")
-                    pure PInt
-
-                bool = do
-                    try (symbol' "BOOLEAN") <|> try (symbol' "BOOL")
-                    pure PBoolean
-
-                real = do
-                    try (symbol' "REAL") <|> try (symbol' "FLOAT4")
-                    pure PReal
-
-                double = do
-                    try (symbol' "DOUBLE PRECISION") <|> try (symbol' "FLOAT8")
-                    pure PDouble
-
-                point = do
-                    try (symbol' "POINT")
-                    pure PPoint
-
-                polygon = do
-                    try (symbol' "POLYGON")
-                    pure PPolygon
-
-                date = do
-                    try (symbol' "DATE")
-                    pure PDate
-
-                binary = do
-                    try (symbol' "BYTEA")
-                    pure PBinary
-
-                time = do
-                    try (symbol' "TIME")
-                    optional do
-                        symbol' "WITHOUT"
-                        symbol' "TIME"
-                        symbol' "ZONE"
-                    pure PTime
-
-                interval = do
-                    try (symbol' "INTERVAL")
-                    fields <- optional do
-                        choice $ map symbol' intervalFields
-                    pure (PInterval fields)
-
-                numericPS = do
-                    try (symbol' "NUMERIC(")
-                    values <- between (space) (char ')' >> space) (varExpr `sepBy` (char ',' >> space))
-                    case values of
-                        [VarExpression precision, VarExpression scale] -> do
-                            let p = textToInt precision
-                            let s = textToInt scale
-                            when (or [isNothing p, isNothing s]) do
-                                Prelude.fail "Failed to parse NUMERIC(..) expression"
-                            pure (PNumeric p s)
-                        [VarExpression precision] -> do
-                            let p = textToInt precision
-                            when (isNothing p) do
-                                Prelude.fail "Failed to parse NUMERIC(..) expression"
-                            pure (PNumeric p Nothing)
-                        _ -> Prelude.fail "Failed to parse NUMERIC(..) expression"
-
-                numeric = do
-                    try (symbol' "NUMERIC")
-                    pure (PNumeric Nothing Nothing)
-
-                varchar = do
-                    try (symbol' "CHARACTER VARYING") <|> try (symbol' "VARCHAR")
-                    value <- optional $ between (char '(' >> space) (char ')' >> space) (varExpr)
-                    case value of
-                        Just (VarExpression limit) -> do
-                            let l = textToInt limit
-                            case l of
-                                Nothing -> Prelude.fail "Failed to parse CHARACTER VARYING(..) expression"
-                                Just l -> pure (PVaryingN (Just l))
-                        Nothing -> pure (PVaryingN Nothing)
-                        _ -> Prelude.fail "Failed to parse CHARACTER VARYING(..) expression"
-
-                character = do
-                    try (symbol' "CHAR(") <|> try (symbol' "CHARACTER(")
-                    value <- between (space) (char ')' >> space) (varExpr)
-                    case value of
-                        VarExpression length -> do
-                            let l = textToInt length
-                            case l of
-                                Nothing -> Prelude.fail "Failed to parse CHARACTER VARYING(..) expression"
-                                Just l -> pure (PCharacterN l)
-                        _ -> Prelude.fail "Failed to parse CHARACTER VARYING(..) expression"
-
-                singleChar = do
-                    try (symbol "\"char\"")
-                    pure PSingleChar
-
-                serial = do
-                    try (symbol' "SERIAL")
-                    pure PSerial
-
-                bigserial = do
-                    try (symbol' "BIGSERIAL")
-                    pure PBigserial
-
-                jsonb = do
-                    try (symbol' "JSONB")
-                    pure PJSONB
-
-                inet = do
-                    try (symbol' "INET")
-                    pure PInet
-
-                tsvector = do
-                    try (symbol' "TSVECTOR")
-                    pure PTSVector
-
-                optionalArray typeParser= do
-                    arrayType <- typeParser;
-                    (try do symbol' "[]"; pure $ PArray arrayType) <|> pure arrayType
-
-                trigger = do
-                    try (symbol' "TRIGGER")
-                    pure PTrigger
-                
-                eventTrigger = do
-                    try (symbol' "EVENT_TRIGGER")
-                    pure PEventTrigger
-
-                customType = do
-                    optional do
-                        lexeme "public"
-                        char '.'
-                    theType <- try (takeWhile1P (Just "Custom type") (\c -> isAlphaNum c || c == '_'))
-                    pure (PCustomType theType)
-
-
-intervalFields :: [Text]
-intervalFields =  [ "YEAR TO MONTH", "DAY TO HOUR", "DAY TO MINUTE", "DAY TO SECOND"
-                   , "HOUR TO MINUTE", "HOUR TO SECOND", "MINUTE TO SECOND"
-                   , "YEAR",  "MONTH", "DAY", "HOUR", "MINUTE", "SECOND"]
-
-
-term = parens expression <|> try callExpr <|> try doubleExpr <|> try intExpr <|> selectExpr <|> varExpr <|> (textExpr <* optional space)
-    where
-        parens f = between (char '(' >> space) (char ')' >> space) f
-
-table = [
-            [ binary  "<>"  NotEqExpression
-            , binary "="  EqExpression
-
-            , binary "<=" LessThanOrEqualToExpression
-            , binary "<"  LessThanExpression
-            , binary ">="  GreaterThanOrEqualToExpression
-            , binary ">"  GreaterThanExpression
-            , binary "||" ConcatenationExpression
-
-            , binary "IS" IsExpression
-            , inExpr
-            , prefix "NOT" NotExpression
-            , prefix "EXISTS" ExistsExpression
-            , typeCast
-            , dot
-            ],
-            [ binary "AND" AndExpression, binary "OR" OrExpression ]
-        ]
-    where
-        binary  name f = InfixL  (f <$ try (symbol name))
-        prefix  name f = Prefix  (f <$ symbol name)
-        postfix name f = Postfix (f <$ symbol name)
-
-        -- Cannot be implemented as a infix operator as that requires two expression operands,
-        -- but the second is the type-cast type which is not an expression
-        typeCast = Postfix do
-            symbol "::"
-            castType <- sqlType
-            pure $ \expr -> TypeCastExpression expr castType
-
-        dot = Postfix do
-            char '.'
-            name <- identifier
-            pure $ \expr -> DotExpression expr name
-        
-        inExpr = Postfix do
-            lexeme "IN"
-            right <- try inArrayExpression <|> expression
-            pure $ \expr -> InExpression expr right
-
--- | Parses a SQL expression
---
--- This parser makes use of makeExprParser as described in https://hackage.haskell.org/package/parser-combinators-1.2.0/docs/Control-Monad-Combinators-Expr.html
-expression :: Parser Expression
-expression = do
-    e <- makeExprParser term table <?> "expression"
-    space
-    pure e
-
-varExpr :: Parser Expression
-varExpr = VarExpression <$> identifier
-
-doubleExpr :: Parser Expression
-doubleExpr = DoubleExpression <$> (Lexer.signed spaceConsumer Lexer.float)
-
-intExpr :: Parser Expression
-intExpr = IntExpression <$> (Lexer.signed spaceConsumer Lexer.decimal)
-
-callExpr :: Parser Expression
-callExpr = do
-    func <- qualifiedIdentifier
-    args <- between (char '(') (char ')') (expression `sepBy` (char ',' >> space))
-    space
-    pure (CallExpression func args)
-
-textExpr :: Parser Expression
-textExpr = TextExpression <$> textExpr'
-
-textExpr' :: Parser Text
-textExpr' = cs <$> do
-    let emptyByteString = do
-            string "'\\x'"
-            pure ""
-    (try (char '\'' *> manyTill Lexer.charLiteral (char '\''))) <|> emptyByteString
-
-selectExpr :: Parser Expression
-selectExpr = do
-    symbol' "SELECT"
-    columns <- expression `sepBy` (char ',' >> space)
-    symbol' "FROM"
-    from <- expression
-
-
-    let whereClause alias = do
-            symbol' "WHERE"
-            whereClause <- expression
-            pure (SelectExpression Select { .. })
-
-    let explicitAs = do
-            symbol' "AS"
-            alias <- identifier
-            whereClause (Just alias)
-
-    let implicitAs = do
-            alias <- identifier
-            whereClause (Just alias)
-
-    whereClause Nothing <|> explicitAs <|> implicitAs
-
-inArrayExpression :: Parser Expression
-inArrayExpression = do
-    values <- between (char '(') (char ')') (expression `sepBy` (char ',' >> space))
-    pure (InArrayExpression values)
-
-
-
-identifier :: Parser Text
-identifier = do
-    i <- (between (char '"') (char '"') (takeWhile1P Nothing (\c -> c /= '"'))) <|> takeWhile1P (Just "identifier") (\c -> isAlphaNum c || c == '_')
-    space
-    pure i
-
-comment = do
-    (char '-' >> char '-') <?> "Line comment"
-    content <- takeWhileP Nothing (/= '\n')
-    pure Comment { content }
-
-createIndex = do
-    lexeme "CREATE"
-    unique <- isJust <$> optional (lexeme "UNIQUE")
-    lexeme "INDEX"
-    indexName <- identifier
-    lexeme "ON"
-    tableName <- qualifiedIdentifier
-    indexType <- optional parseIndexType
-    columns <- between (char '(' >> space) (char ')' >> space) parseIndexColumns
-    whereClause <- optional do
-        lexeme "WHERE"
-        expression
-    char ';'
-    pure CreateIndex { indexName, unique, tableName, columns, whereClause, indexType }
-
-parseIndexColumns = parseIndexColumn `sepBy` (char ',' >> space)
-
-parseIndexColumn = do
-    column <- expression
-    orderOption1 <- optional $ space *> lexeme "ASC" $> Asc <|> space *> lexeme "DESC" $> Desc
-    orderOption2 <- optional $ space *> lexeme "NULLS FIRST" $> NullsFirst <|> space *> lexeme "NULLS LAST" $> NullsLast
-    pure IndexColumn { column, columnOrder = catMaybes [orderOption1, orderOption2] }
-
-parseIndexType = do
-    lexeme "USING"
-
-    choice $ map (\(s, v) -> do symbol' s; pure v)
-        [ ("btree", Btree)
-        , ("gin", Gin)
-        , ("gist", Gist)
-        ]
-
-createFunction = do
-    lexeme "CREATE"
-    orReplace <- isJust <$> optional (lexeme "OR" >> lexeme "REPLACE")
-    lexeme "FUNCTION"
-    functionName <- qualifiedIdentifier
-    functionArguments <- between (char '(') (char ')') (functionArgument `sepBy` (char ',' >> space))
-    space
-    lexeme "RETURNS"
-    returns <- sqlType
-    space
-
-    language <- optional do
-        lexeme "language" <|> lexeme "LANGUAGE"
-        symbol' "plpgsql" <|> symbol' "SQL"
-
-    lexeme "AS"
-    space
-    functionBody <- cs <$> between (char '$' >> char '$') (char '$' >> char '$') (many (anySingleBut '$'))
-    space
-
-    language <- case language of
-        Just language -> pure language
-        Nothing -> do
-            lexeme "language" <|> lexeme "LANGUAGE"
-            symbol' "plpgsql" <|> symbol' "SQL"
-    char ';'
-    pure CreateFunction { functionName, functionArguments, functionBody, orReplace, returns, language }
-    where
-        functionArgument = do
-            argumentName <- qualifiedIdentifier
-            space
-            argumentType <- sqlType
-            pure (argumentName, argumentType)
-
-createTrigger = do
-    lexeme "CREATE"
-    createEventTrigger <|> createTrigger'
-
-createEventTrigger = do
-    lexeme "EVENT"
-    lexeme "TRIGGER"
-
-    name <- qualifiedIdentifier
-    lexeme "ON"
-    eventOn <- identifier
-
-    whenCondition <- optional do
-        lexeme "WHEN"
-        expression
-
-    lexeme "EXECUTE"
-    (lexeme "FUNCTION") <|> (lexeme "PROCEDURE")
-
-    (CallExpression functionName arguments) <- callExpr
-
-    char ';'
-
-    pure CreateEventTrigger
-        { name
-        , eventOn
-        , whenCondition
-        , functionName
-        , arguments
-        }
-
-
-
-createTrigger' = do
-    lexeme "TRIGGER"
-
-    name <- qualifiedIdentifier
-    eventWhen <- (lexeme "AFTER" >> pure After) <|> (lexeme "BEFORE" >> pure Before) <|> (lexeme "INSTEAD OF" >> pure InsteadOf)
-    event <- (lexeme "INSERT" >> pure TriggerOnInsert) <|> (lexeme "UPDATE" >> pure TriggerOnUpdate) <|> (lexeme "DELETE" >> pure TriggerOnDelete) <|> (lexeme "TRUNCATE" >> pure TriggerOnTruncate)
-
-    lexeme "ON"
-    tableName <- qualifiedIdentifier
-
-    lexeme "FOR"
-    optional (lexeme "EACH")
-
-    for <- (lexeme "ROW" >> pure ForEachRow) <|> (lexeme "STATEMENT" >> pure ForEachStatement)
-
-    whenCondition <- optional do
-        lexeme "WHEN"
-        expression
-
-    lexeme "EXECUTE"
-    optional (lexeme "FUNCTION" <|> lexeme "PROCEDURE")
-
-    (CallExpression functionName arguments) <- callExpr
-
-    char ';'
-
-    pure CreateTrigger
-        { name
-        , eventWhen
-        , event
-        , tableName
-        , for
-        , whenCondition
-        , functionName
-        , arguments
-        }
-
-alterTable = do
-    lexeme "TABLE"
-    optional (lexeme "ONLY")
-    tableName <- qualifiedIdentifier
-    let add = do
-            lexeme "ADD"
-            let addUnique = do
-                    unique <- parseUniqueConstraint Nothing
-                    deferrable <- optional parseDeferrable
-                    deferrableType <- optional parseDeferrableType
-                    char ';'
-                    pure (AddConstraint tableName unique deferrable deferrableType)
-            addConstraint tableName <|> addColumn tableName <|> addUnique
-    let drop = do
-            lexeme "DROP"
-            dropColumn tableName <|> dropConstraint tableName
-    let rename = do
-            lexeme "RENAME"
-            renameColumn tableName <|> renameTable tableName
-    let alter = do
-            lexeme "ALTER"
-            alterColumn tableName
-    enableRowLevelSecurity tableName <|> add <|> drop <|> rename <|> alter
-
-alterType = do
-    lexeme "TYPE"
-    typeName <- qualifiedIdentifier
-    addValue typeName
-
-alterSequence = do
-    lexeme "SEQUENCE"
-    raw <- cs <$> someTill (anySingle) (char ';')
-    pure UnknownStatement { raw = "ALTER SEQUENCE " <> raw };
-
--- | ALTER TABLE users ALTER COLUMN email DROP NOT NULL;
---  ALTER TABLE users ALTER COLUMN email SET NOT NULL;
---  ALTER TABLE users ALTER COLUMN email SET DEFAULT 'value';
---  ALTER TABLE users ALTER COLUMN email DROP DEFAULT;
-alterColumn tableName = do
-    lexeme "COLUMN"
-    columnName <- identifier
-
-    let drop = do
-            lexeme "DROP"
-            let notNull = do
-                    lexeme "NOT"
-                    lexeme "NULL"
-                    char ';'
-                    pure DropNotNull { tableName, columnName }
-            let defaultValue = do
-                    lexeme "DEFAULT"
-                    char ';'
-                    pure DropDefaultValue { tableName, columnName }
-            notNull <|> defaultValue
-
-    let set = do
-            lexeme "SET"
-            let notNull = do
-                    lexeme "NOT"
-                    lexeme "NULL"
-                    char ';'
-                    pure SetNotNull { tableName, columnName }
-            let defaultValue = do
-                    lexeme "DEFAULT"
-                    value <- expression
-                    char ';'
-                    pure SetDefaultValue { tableName, columnName, value }
-            notNull <|> defaultValue
-
-    drop <|> set
-
-
-
-
-enableRowLevelSecurity tableName = do
-    lexeme "ENABLE"
-    lexeme "ROW"
-    lexeme "LEVEL"
-    lexeme "SECURITY"
-    char ';'
-    pure EnableRowLevelSecurity { tableName }
-
-createPolicy = do
-    lexeme "CREATE"
-    lexeme "POLICY"
-    name <- identifier
-    lexeme "ON"
-    tableName <- qualifiedIdentifier
-
-    action <- optional (lexeme "FOR" >> policyAction)
-
-    using <- optional do
-        lexeme "USING"
-        expression
-
-    check <- optional do
-        lexeme "WITH"
-        lexeme "CHECK"
-        expression
-
-    char ';'
-
-    pure CreatePolicy { name, action, tableName, using, check }
-
-policyAction =
-    (lexeme "ALL" >> pure PolicyForAll)
-    <|> (lexeme "SELECT" >> pure PolicyForSelect)
-    <|> (lexeme "INSERT" >> pure PolicyForInsert)
-    <|> (lexeme "UPDATE" >> pure PolicyForUpdate)
-    <|> (lexeme "DELETE" >> pure PolicyForDelete)
-
-setStatement = do
-    lexeme "SET"
-    name <- identifier
-    lexeme "="
-    value <- expression
-    char ';'
-    pure Set { name, value }
-
-selectStatement = do
-    lexeme "SELECT"
-    query <- takeWhile1P (Just "SQL Query") (\c -> c /= ';')
-    char ';'
-    pure SelectStatement { query }
-
-
-commentStatement = do
-    lexeme "COMMENT"
-    content <- takeWhile1P (Just "SQL Query") (\c -> c /= ';')
-    char ';'
-    pure Comment { content }
-
-qualifiedIdentifier = do
-    optional $ try do
-        lexeme "public"
-        char '.'
-    identifier
-
-addColumn tableName = do
-    lexeme "COLUMN"
-    (_, column) <- parseColumn
-    char ';'
-    pure AddColumn { tableName, column }
-
-dropColumn tableName = do
-    lexeme "COLUMN"
-    columnName <- identifier
-    char ';'
-    pure DropColumn { tableName, columnName }
-
-dropConstraint tableName = do
-    lexeme "CONSTRAINT"
-    constraintName <- identifier
-    char ';'
-    pure DropConstraint { tableName, constraintName }
-
-renameColumn tableName = do
-    lexeme "COLUMN"
-    from <- identifier
-    lexeme "TO"
-    to <- identifier
-    char ';'
-    pure RenameColumn { tableName, from, to }
-
-renameTable tableName = do
-    lexeme "TO"
-    to <- identifier
-    char ';'
-    pure RenameTable { from = tableName, to }
-
-dropTable = do
-    lexeme "DROP"
-    lexeme "TABLE"
-    tableName <- identifier
-    char ';'
-    pure DropTable { tableName }
-
-dropType = do
-    lexeme "DROP"
-    lexeme "TYPE"
-    name <- qualifiedIdentifier
-    char ';'
-    pure DropEnumType { name }
-
-dropFunction = do
-    lexeme "DROP"
-    lexeme "FUNCTION"
-    functionName <- qualifiedIdentifier
-    char ';'
-    pure DropFunction { functionName }
-
-dropIndex = do
-    lexeme "DROP"
-    lexeme "INDEX"
-    indexName <- qualifiedIdentifier
-    char ';'
-    pure DropIndex { indexName }
-
-dropPolicy = do
-    lexeme "DROP"
-    lexeme "POLICY"
-    policyName <- qualifiedIdentifier
-    lexeme "ON"
-    tableName <- qualifiedIdentifier
-    char ';'
-    pure DropPolicy { tableName, policyName }
-
-dropTrigger = do
-    lexeme "DROP"
-
-    dropEventTrigger <|> dropTrigger'
-
-dropTrigger' = do
-    lexeme "TRIGGER"
-    name <- qualifiedIdentifier
-    lexeme "ON"
-    tableName <- qualifiedIdentifier
-    char ';'
-    pure DropTrigger { name, tableName }
-
-
-dropEventTrigger = do
-    lexeme "EVENT"
-    lexeme "TRIGGER"
-    name <- qualifiedIdentifier
-    char ';'
-    pure DropEventTrigger { name }
-
-createSequence = do
-    lexeme "CREATE"
-    lexeme "SEQUENCE"
-    name <- qualifiedIdentifier
-
-    -- We accept all the following SEQUENCE attributes, but don't save them
-    -- This is mostly to void issues in migrations when parsing the pg_dump output
-    optional do
-        lexeme "AS"
-        sqlType
-
-    optional do
-        lexeme "START"
-        lexeme "WITH"
-        expression
-
-    optional do
-        lexeme "INCREMENT"
-        lexeme "BY"
-        expression
-
-    optional do
-        lexeme "NO"
-        lexeme "MINVALUE"
-
-    optional do
-        lexeme "NO"
-        lexeme "MAXVALUE"
-
-    optional do
-        lexeme "CACHE"
-        expression
-
-    char ';'
-    pure CreateSequence { name }
-
-addValue typeName = do
-    lexeme "ADD"
-    lexeme "VALUE"
-    ifNotExists <- isJust <$> optional do
-            lexeme "IF"
-            lexeme "NOT"
-            lexeme "EXISTS"
-    newValue <- textExpr'
-    char ';'
-    pure AddValueToEnumType { enumName = typeName, newValue, ifNotExists }
-
-begin = do
-    lexeme "BEGIN"
-    char ';'
-    pure Begin
-
-commit = do
-    lexeme "COMMIT"
-    char ';'
-    pure Commit
-
--- | Turns sql like '1::double precision' into just '1'
-removeTypeCasts :: Expression -> Expression
-removeTypeCasts (TypeCastExpression value _) = value
-removeTypeCasts otherwise = otherwise
-
-restrict = do
-    lexeme "\\restrict"
-    key <- identifier
-    pure Comment { content = "" }
-
-unrestrict = do
-    lexeme "\\unrestrict"
-    key <- identifier
-    pure Comment { content = "" }
diff --git a/IHP/IDE/SchemaDesigner/SchemaOperations.hs b/IHP/IDE/SchemaDesigner/SchemaOperations.hs
--- a/IHP/IDE/SchemaDesigner/SchemaOperations.hs
+++ b/IHP/IDE/SchemaDesigner/SchemaOperations.hs
@@ -6,7 +6,7 @@
 module IHP.IDE.SchemaDesigner.SchemaOperations where
 
 import IHP.Prelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import Data.Maybe (fromJust)
 import qualified Data.List as List
 import qualified Data.Text as Text
@@ -15,14 +15,17 @@
 type Schema = [Statement]
 
 -- | Creates a new tables with a 'id' columns as the primary key
-addTable :: Text -> Schema -> Schema
-addTable tableName list = list <> [StatementCreateTable CreateTable
+--
+-- The second parameter is the UUID function name to use as default value
+-- (e.g. @"uuid_generate_v4"@ or @"uuidv7"@).
+addTable :: Text -> Text -> Schema -> Schema
+addTable tableName uuidFunction list = list <> [StatementCreateTable CreateTable
     { name = tableName
     , columns =
         [Column
             { name = "id"
             , columnType = PUUID
-            , defaultValue = Just (CallExpression "uuid_generate_v4" [])
+            , defaultValue = Just (CallExpression uuidFunction [])
             , notNull = True
             , isUnique = False
             , generator = Nothing
@@ -30,6 +33,7 @@
     , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
     , constraints = []
     , unlogged = False
+    , inherits = Nothing
     }]
 
 
@@ -443,8 +447,11 @@
                         otheriwse                                   -> False
                     |> fmap \case
                         StatementCreateTable table -> table
+                        _ -> error "resolveFK: expected StatementCreateTable"
+            resolveFK _ = Nothing
 
             emptyPolicy = CreatePolicy { name = "", action = Nothing, tableName, using = Nothing, check = Nothing }
+suggestPolicy _ _ = error "suggestPolicy: expected StatementCreateTable"
 
 isUserIdColumn :: Column -> Bool
 isUserIdColumn Column { name = "user_id" } = True
@@ -490,7 +497,7 @@
         trigger = CreateTrigger
             { name = updatedAtTriggerName tableName
             , eventWhen = Before
-            , event = TriggerOnUpdate
+            , event = [TriggerOnUpdate]
             , tableName
             , for = ForEachRow
             , whenCondition = Nothing
@@ -525,6 +532,7 @@
                 , orReplace = False
                 , returns = PTrigger
                 , language = "plpgsql"
+                , securityDefiner = False
                 }
 
 deleteTriggerIfExists :: Text -> [Statement] -> [Statement]
@@ -565,6 +573,7 @@
                 (Just using, Nothing) -> not (isRef using)
                 (Nothing, Just check) -> not (isRef check)
                 (Just using, Just check) -> not (isRef using && isRef check)
+                (Nothing, Nothing) -> True
             where
                 isRef :: Expression -> Bool
                 isRef (TextExpression {}) = False
@@ -575,6 +584,7 @@
                 isRef (AndExpression a b) = isRef a || isRef b
                 isRef (IsExpression a b) = isRef a || isRef b
                 isRef (InExpression a b) = isRef a || isRef b
+                isRef (InArrayExpression exprs) = foldl' (||) False (map isRef exprs)
                 isRef (NotExpression a) = isRef a
                 isRef (ExistsExpression a) = isRef a
                 isRef (OrExpression a b) = isRef a || isRef b
@@ -633,12 +643,21 @@
             EqExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
             AndExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
             IsExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
+            InExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
+            InArrayExpression exprs -> exprs |> map expressionReferencesColumn |> List.or
             NotExpression a -> expressionReferencesColumn a
+            ExistsExpression a -> expressionReferencesColumn a
             OrExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
             LessThanExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
             LessThanOrEqualToExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
             GreaterThanExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
             GreaterThanOrEqualToExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
+            DoubleExpression _ -> False
+            IntExpression _ -> False
+            TypeCastExpression a _ -> expressionReferencesColumn a
+            SelectExpression _ -> False
+            DotExpression a _ -> expressionReferencesColumn a
+            ConcatenationExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
 
 doesHaveExistingPolicies :: [Statement] -> Text -> Bool
 doesHaveExistingPolicies statements tableName = statements
diff --git a/IHP/IDE/SchemaDesigner/Types.hs b/IHP/IDE/SchemaDesigner/Types.hs
--- a/IHP/IDE/SchemaDesigner/Types.hs
+++ b/IHP/IDE/SchemaDesigner/Types.hs
@@ -2,272 +2,11 @@
 Module: IHP.IDE.SchemaDesigner.Types
 Description: Types for representing an AST of SQL DDL
 Copyright: (c) digitally induced GmbH, 2020
--}
-module IHP.IDE.SchemaDesigner.Types where
 
-import IHP.Prelude
-
-data Statement
-    =
-    -- | CREATE TABLE name ( columns );
-      StatementCreateTable { unsafeGetCreateTable :: CreateTable }
-    -- | CREATE TYPE name AS ENUM ( values );
-    | CreateEnumType { name :: Text, values :: [Text] }
-    -- | DROP TYPE name;
-    | DropEnumType { name :: Text }
-    -- | CREATE EXTENSION IF NOT EXISTS "name";
-    | CreateExtension { name :: Text, ifNotExists :: Bool }
-    -- | ALTER TABLE tableName ADD CONSTRAINT constraint;
-    | AddConstraint { tableName :: Text, constraint :: Constraint, deferrable :: Maybe Bool, deferrableType :: Maybe DeferrableType }
-    -- | ALTER TABLE tableName DROP CONSTRAINT constraintName;
-    | DropConstraint { tableName, constraintName :: Text }
-    -- | ALTER TABLE tableName ADD COLUMN column;
-    | AddColumn { tableName :: Text, column :: Column }
-    -- | ALTER TABLE tableName DROP COLUMN columnName;
-    | DropColumn { tableName :: Text, columnName :: Text }
-    -- | DROP TABLE tableName;
-    | DropTable { tableName :: Text }
-    | UnknownStatement { raw :: Text }
-    | Comment { content :: Text }
-    -- | CREATE INDEX indexName ON tableName (columnName); CREATE INDEX indexName ON tableName (LOWER(columnName));
-    -- | CREATE UNIQUE INDEX name ON table (column [, ...]);
-    | CreateIndex { indexName :: Text, unique :: Bool, tableName :: Text, columns :: [IndexColumn], whereClause :: Maybe Expression, indexType :: Maybe IndexType }
-    -- | DROP INDEX indexName;
-    | DropIndex { indexName :: Text }
-    -- | CREATE OR REPLACE FUNCTION functionName(param1 TEXT, param2 INT) RETURNS TRIGGER AS $$functionBody$$ language plpgsql;
-    | CreateFunction { functionName :: Text, functionArguments :: [(Text, PostgresType)], functionBody :: Text, orReplace :: Bool, returns :: PostgresType, language :: Text }
-    -- | ALTER TABLE tableName ENABLE ROW LEVEL SECURITY;
-    | EnableRowLevelSecurity { tableName :: Text }
-    -- CREATE POLICY name ON tableName USING using WITH CHECK check;
-    | CreatePolicy { name :: Text, tableName :: Text, action :: Maybe PolicyAction, using :: Maybe Expression, check :: Maybe Expression }
-    -- SET name = value;
-    | Set { name :: Text, value :: Expression }
-    -- SELECT query;
-    | SelectStatement { query :: Text }
-    -- CREATE SEQUENCE name;
-    | CreateSequence { name :: Text }
-    -- ALTER TABLE tableName RENAME COLUMN from TO to;
-    | RenameColumn { tableName :: Text, from :: Text, to :: Text }
-    -- ALTER TYPE enumName ADD VALUE newValue;
-    | AddValueToEnumType { enumName :: Text, newValue :: Text, ifNotExists :: Bool }
-    -- ALTER TABLE tableName ALTER COLUMN columnName DROP NOT NULL;
-    | DropNotNull { tableName :: Text, columnName :: Text }
-    -- ALTER TABLE tableName ALTER COLUMN columnName SET NOT NULL;
-    | SetNotNull { tableName :: Text, columnName :: Text }
-    -- | ALTER TABLE from RENAME TO to;
-    | RenameTable { from :: Text, to :: Text }
-    -- | DROP POLICY policyName ON tableName;
-    | DropPolicy { tableName :: Text, policyName :: Text }
-    -- ALTER TABLE tableName ALTER COLUMN columnName SET DEFAULT 'value';
-    | SetDefaultValue { tableName :: Text, columnName :: Text, value :: Expression }
-    -- ALTER TABLE tableName ALTER COLUMN columnName DROP DEFAULT;
-    | DropDefaultValue { tableName :: Text, columnName :: Text }
-    -- | CREATE TRIGGER ..;
-    | CreateTrigger { name :: !Text, eventWhen :: !TriggerEventWhen, event :: !TriggerEvent, tableName :: !Text, for :: !TriggerFor, whenCondition :: Maybe Expression, functionName :: !Text, arguments :: ![Expression] }
-    -- | CREATE EVENT TRIGGER ..;
-    | CreateEventTrigger { name :: !Text, eventOn :: !Text, whenCondition :: Maybe Expression, functionName :: !Text, arguments :: ![Expression] }
-    -- | DROP TRIGGER .. ON ..;
-    | DropTrigger { name :: !Text, tableName :: !Text }
-    -- | DROP EVENT TRIGGER ..;
-    | DropEventTrigger { name :: !Text }
-    -- | BEGIN;
-    | Begin
-    -- | COMMIT;
-    | Commit
-    | DropFunction { functionName :: !Text }
-    deriving (Eq, Show)
-
-data DeferrableType
-    = InitiallyImmediate
-    | InitiallyDeferred
-    deriving (Eq, Show)
-
-data CreateTable
-  = CreateTable
-      { name :: Text
-      , columns :: [Column]
-      , primaryKeyConstraint :: PrimaryKeyConstraint
-      , constraints :: [Constraint]
-      , unlogged :: !Bool
-      }
-  deriving (Eq, Show)
-
-data Column = Column
-    { name :: Text
-    , columnType :: PostgresType
-    , defaultValue :: Maybe Expression
-    , notNull :: Bool
-    , isUnique :: Bool
-    , generator :: Maybe ColumnGenerator
-    }
-    deriving (Eq, Show)
-
-data OnDelete
-    = NoAction
-    | Restrict
-    | SetNull
-    | SetDefault
-    | Cascade
-    deriving (Show, Eq)
-
-data ColumnGenerator
-    = ColumnGenerator
-    { generate :: !Expression
-    , stored :: !Bool
-    } deriving (Show, Eq)
-
-newtype PrimaryKeyConstraint
-  = PrimaryKeyConstraint { primaryKeyColumnNames :: [Text] }
-  deriving (Eq, Show)
-
-data Constraint
-    -- | FOREIGN KEY (columnName) REFERENCES referenceTable (referenceColumn) ON DELETE onDelete;
-    = ForeignKeyConstraint
-        { name :: !(Maybe Text)
-        , columnName :: !Text
-        , referenceTable :: !Text
-        , referenceColumn :: !(Maybe Text)
-        , onDelete :: !(Maybe OnDelete)
-        }
-    | UniqueConstraint
-        { name :: !(Maybe Text)
-        , columnNames :: ![Text]
-        }
-    | CheckConstraint
-        { name :: !(Maybe Text)
-        , checkExpression :: !Expression
-        }
-    | ExcludeConstraint
-        { name :: !(Maybe Text)
-        , excludeElements :: ![ExcludeConstraintElement]
-        , predicate :: !(Maybe Expression)
-        , indexType :: !(Maybe IndexType)
-        }
-    | AlterTableAddPrimaryKey
-        { name :: !(Maybe Text)
-        , primaryKeyConstraint :: !PrimaryKeyConstraint
-        }
-    deriving (Eq, Show)
-
-data ExcludeConstraintElement = ExcludeConstraintElement { element :: !Text, operator :: !Text }
-    deriving (Eq, Show)
-
-data Expression =
-    -- | Sql string like @'hello'@
-    TextExpression Text
-    -- | Simple variable like @users@
-    | VarExpression Text
-    -- | Simple call, like @COALESCE(name, 'unknown name')@
-    | CallExpression Text [Expression]
-    -- | Not equal operator, a <> b
-    | NotEqExpression Expression Expression
-    -- | Equal operator, a = b
-    | EqExpression Expression Expression
-    -- | a AND b
-    | AndExpression Expression Expression
-    -- | a IS b
-    | IsExpression Expression Expression
-    -- | a IN b
-    | InExpression Expression Expression
-    -- | ('a', 'b')
-    | InArrayExpression [Expression]
-    -- | NOT a
-    | NotExpression Expression
-    -- | EXISTS a
-    | ExistsExpression Expression
-    -- | a OR b
-    | OrExpression Expression Expression
-    -- | a < b
-    | LessThanExpression Expression Expression
-    -- | a <= b
-    | LessThanOrEqualToExpression Expression Expression
-    -- | a > b
-    | GreaterThanExpression Expression Expression
-    -- | a >= b
-    | GreaterThanOrEqualToExpression Expression Expression
-    -- | Double literal value, e.g. 0.1337
-    | DoubleExpression Double
-    -- | Integer literal value, e.g. 1337
-    | IntExpression Int
-    -- | value::type
-    | TypeCastExpression Expression PostgresType
-    | SelectExpression Select
-    | DotExpression Expression Text
-    | ConcatenationExpression Expression Expression -- ^ a || b
-    deriving (Eq, Show)
-
-data Select = Select
-    { columns :: [Expression]
-    , from :: Expression
-    , alias :: Maybe Text
-    , whereClause :: Expression
-    } deriving (Eq, Show)
-
-data PostgresType
-    = PUUID
-    | PText
-    | PInt
-    | PSmallInt
-    | PBigInt
-    | PBoolean
-    | PTimestampWithTimezone
-    | PTimestamp
-    | PReal
-    | PDouble
-    | PPoint
-    | PPolygon
-    | PDate
-    | PBinary
-    | PTime
-    | PInterval { fields :: Maybe Text }
-    | PNumeric { precision :: Maybe Int, scale :: Maybe Int }
-    | PVaryingN (Maybe Int)
-    | PCharacterN Int
-    | PSingleChar
-    | PSerial
-    | PBigserial
-    | PJSONB
-    | PInet
-    | PTSVector
-    | PArray PostgresType
-    | PTrigger
-    | PEventTrigger
-    | PCustomType Text
-    deriving (Eq, Show)
-
-data TriggerEventWhen
-    = Before
-    | After
-    | InsteadOf
-    deriving (Eq, Show)
-
-data TriggerEvent
-    = TriggerOnInsert
-    | TriggerOnUpdate
-    | TriggerOnDelete
-    | TriggerOnTruncate
-    deriving (Eq, Show)
-
-data TriggerFor
-    = ForEachRow
-    | ForEachStatement
-    deriving (Eq, Show)
-
-data PolicyAction
-    = PolicyForAll
-    | PolicyForSelect
-    | PolicyForInsert
-    | PolicyForUpdate
-    | PolicyForDelete
-    deriving (Eq, Show)
-
-data IndexType = Btree | Gin | Gist
-    deriving (Eq, Show)
-
-data IndexColumn
-    = IndexColumn { column :: Expression, columnOrder :: [IndexColumnOrder] }
-    deriving (Eq, Show)
+This module re-exports types from 'IHP.Postgres.Types' for backwards compatibility.
+-}
+module IHP.IDE.SchemaDesigner.Types
+( module IHP.Postgres.Types
+) where
 
-data IndexColumnOrder
-    = Asc | Desc | NullsFirst | NullsLast
-    deriving (Eq, Show)
+import IHP.Postgres.Types
diff --git a/IHP/IDE/SchemaDesigner/View/Columns/Edit.hs b/IHP/IDE/SchemaDesigner/View/Columns/Edit.hs
--- a/IHP/IDE/SchemaDesigner/View/Columns/Edit.hs
+++ b/IHP/IDE/SchemaDesigner/View/Columns/Edit.hs
@@ -1,8 +1,8 @@
 module IHP.IDE.SchemaDesigner.View.Columns.Edit where
 
 import IHP.ViewPrelude hiding (primaryKeyColumnNames)
-import IHP.IDE.SchemaDesigner.Types
-import qualified IHP.IDE.SchemaDesigner.Compiler as Compiler
+import IHP.Postgres.Types
+import qualified IHP.Postgres.Compiler as Compiler
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 import Text.Countable (singularize)
@@ -17,7 +17,7 @@
 
 instance View EditColumnView where
     html EditColumnView { column = column@Column { name }, .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just tableName)}
             {renderColumnSelector tableName (zip [0..] columns) statements}
         </div>
@@ -26,7 +26,7 @@
     |]
         where
             table = findStatementByName tableName statements
-            columns = maybe [] ((.columns) . unsafeGetCreateTable) table
+            columns = getTableColumns tableName statements
             primaryKeyColumns = maybe [] (primaryKeyColumnNames . (.primaryKeyConstraint) . unsafeGetCreateTable) table
 
             isArrayType (PArray _) = True
@@ -40,7 +40,7 @@
                     <input type="hidden" name="tableName" value={tableName}/>
                     <input type="hidden" name="columnId" value={tshow columnId}/>
 
-                    <div class="form-group">
+                    <div class="mb-3">
                         <input
                             id="nameInput"
                             name="name"
@@ -52,45 +52,45 @@
                             />
                     </div>
 
-                    <div class="form-group">
+                    <div class="mb-3">
                         {typeSelector (Just (column.columnType)) enumNames}
 
                         <div class="d-flex text-muted mt-1" id="column-options">
-                            <div class="custom-control custom-checkbox mr-2">
-                                <input id="allowNull" type="checkbox" name="allowNull" class="custom-control-input" checked={not (column.notNull)}/>
-                                <label class="mr-1 custom-control-label" for="allowNull">
+                            <div class="form-check me-2">
+                                <input id="allowNull" type="checkbox" name="allowNull" class="form-check-input" checked={not (column.notNull)}/>
+                                <label class="me-1 form-check-label" for="allowNull">
                                     Nullable
                                 </label>
                             </div>
 
-                            <div class="custom-control custom-checkbox mr-2">
-                                <input type="checkbox" id="isUnique" name="isUnique" class="custom-control-input" checked={column.isUnique}/>
-                                <label class="custom-control-label" for="isUnique">
+                            <div class="form-check me-2">
+                                <input type="checkbox" id="isUnique" name="isUnique" class="form-check-input" checked={column.isUnique}/>
+                                <label class="form-check-label" for="isUnique">
                                     Unique
                                 </label>
                             </div>
 
-                            <div class="custom-control custom-checkbox mr-2">
-                                <input type="checkbox" id="primaryKey" name="primaryKey" class="custom-control-input" checked={isPrimaryKey}/>
-                                <label class="custom-control-label" for="primaryKey">
+                            <div class="form-check me-2">
+                                <input type="checkbox" id="primaryKey" name="primaryKey" class="form-check-input" checked={isPrimaryKey}/>
+                                <label class="form-check-label" for="primaryKey">
                                     Primary Key
                                 </label>
                             </div>
 
-                            <div class="custom-control custom-checkbox mr-2">
-                                <input id="isArray" type="checkbox" name="isArray" class="custom-control-input" checked={isArrayType (column.columnType)}/>
-                                <label class="custom-control-label">
+                            <div class="form-check me-2">
+                                <input id="isArray" type="checkbox" name="isArray" class="form-check-input" checked={isArrayType (column.columnType)}/>
+                                <label class="form-check-label">
                                      Array Type
                                 </label>
                             </div>
                         </div>
                     </div>
 
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         {defaultSelector (column.defaultValue)}
                     </div>
 
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Edit Column</button>
                     </div>
                     <input type="hidden" name="primaryKey" value={inputValue False}/>
diff --git a/IHP/IDE/SchemaDesigner/View/Columns/EditForeignKey.hs b/IHP/IDE/SchemaDesigner/View/Columns/EditForeignKey.hs
--- a/IHP/IDE/SchemaDesigner/View/Columns/EditForeignKey.hs
+++ b/IHP/IDE/SchemaDesigner/View/Columns/EditForeignKey.hs
@@ -1,9 +1,10 @@
 module IHP.IDE.SchemaDesigner.View.Columns.EditForeignKey where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
+import IHP.IDE.SchemaDesigner.View.Columns.NewForeignKey (foreignKeyFormModal)
 
 data EditForeignKeyView = EditForeignKeyView
     { statements :: [Statement]
@@ -17,7 +18,7 @@
 
 instance View EditForeignKeyView where
     html EditForeignKeyView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just tableName)}
             {renderColumnSelector tableName  (zip [0..] columns) statements}
         </div>
@@ -25,62 +26,14 @@
         {renderModal modal}
     |]
         where
-            table = findStatementByName tableName statements
-            columns = maybe [] ((.columns) . unsafeGetCreateTable) table
-
-            modalContent = [hsx|
-                <form method="POST" action={UpdateForeignKeyAction}>
-                    <input type="hidden" name="tableName" value={tableName}/>
-                    <input type="hidden" name="columnName" value={columnName}/>
-
-                    <div class="form-group row">
-                        <label class="col-sm-2 col-form-label">Reference Table:</label>
-                        <div class="col-sm-10">
-                            <select name="referenceTable" class="form-control select2" autofocus="autofocus">
-                                {forEach tableNames renderTableNameSelector}
-                            </select>
-                        </div>
-                    </div>
-
-                    <div class="form-group row">
-                        <label class="col-sm-2 col-form-label">Name:</label>
-                        <div class="col-sm-10">
-                            <input name="constraintName" type="text" class="form-control" value={constraintName}/>
-                        </div>
-                    </div>
-
-                    <div class="form-group row">
-                        <label class="col-sm-2 col-form-label">On Delete:</label>
-                        <div class="col-sm-10">
-                            <select name="onDelete" class="form-control select2">
-                                {onDeleteSelector "NoAction"}
-                                {onDeleteSelector "Restrict"}
-                                {onDeleteSelector "SetNull"}
-                                {onDeleteSelector "SetDefault"}
-                                {onDeleteSelector "Cascade"}
-                            </select>
-                        </div>
-                    </div>
-
-                    <div class="text-right">
-                        <button type="submit" class="btn btn-primary">Edit Constraint</button>
-                    </div>
-                </form>
-                {select2}
-            |]
-                where
-                    renderTableNameSelector tableName = if tableName == referenceTable
-                        then preEscapedToHtml [plain|<option selected>#{tableName}</option>|]
-                        else preEscapedToHtml [plain|<option>#{tableName}</option>|]
-                    onDeleteSelector option = if option == onDelete
-                        then preEscapedToHtml [plain|<option selected>#{option}</option>|]
-                        else preEscapedToHtml [plain|<option>#{option}</option>|]
-                    select2 = preEscapedToHtml [plain|
-                        <script>
-                            $('.select2').select2();
-                        </script>
-                    |]
-            modalFooter = mempty 
-            modalCloseUrl = pathTo ShowTableAction { tableName }
-            modalTitle = "Edit Foreign Key Constraint"
-            modal = Modal { modalContent, modalFooter, modalCloseUrl, modalTitle }
+            columns = getTableColumns tableName statements
+            modal = foreignKeyFormModal
+                (pathTo UpdateForeignKeyAction)
+                tableName
+                columnName
+                tableNames
+                constraintName
+                (Just referenceTable)
+                onDelete
+                "Edit Constraint"
+                "Edit Foreign Key Constraint"
diff --git a/IHP/IDE/SchemaDesigner/View/Columns/New.hs b/IHP/IDE/SchemaDesigner/View/Columns/New.hs
--- a/IHP/IDE/SchemaDesigner/View/Columns/New.hs
+++ b/IHP/IDE/SchemaDesigner/View/Columns/New.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Columns.New where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.ToolServer.Routes ()
 import IHP.IDE.SchemaDesigner.View.Layout
@@ -17,7 +17,7 @@
 
 instance View NewColumnView where
     html NewColumnView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just tableName)}
             {renderColumnSelector tableName  (zip [0..] columns) statements}
         </div>
@@ -25,15 +25,14 @@
         {renderModal modal}
     |]
         where
-            table = findStatementByName tableName statements
-            columns = maybe [] ((.columns) . unsafeGetCreateTable) table
+            columns = getTableColumns tableName statements
 
             modalContent = [hsx|
                 {renderFlashMessages}
                 <form method="POST" action={CreateColumnAction} id="new-column">
                     <input type="hidden" name="tableName" value={tableName}/>
 
-                    <div class="form-group">
+                    <div class="mb-3">
                         <input
                             id="colName"
                             name="name"
@@ -45,35 +44,35 @@
                             />
                     </div>
 
-                    <div class="form-group">
+                    <div class="mb-3">
                         {typeSelector Nothing enumNames}
                         <div class="mt-1 text-muted" id="column-options">
                             {generateReferenceCheckboxes}
                             <div class="d-flex">
-                                <div class="custom-control custom-checkbox">
-                                    <input id="allowNull" type="checkbox" name="allowNull" class="mr-1 custom-control-input"/>
-                                    <label class="custom-control-label mr-2" for="allowNull">
+                                <div class="form-check">
+                                    <input id="allowNull" type="checkbox" name="allowNull" class="me-1 form-check-input"/>
+                                    <label class="form-check-label me-2" for="allowNull">
                                         Nullable
                                     </label>
                                 </div>
 
-                                <div class="custom-control custom-checkbox">
-                                    <input type="checkbox" id="isUnique" name="isUnique" class="mr-1 custom-control-input"/>
-                                    <label class="mx-2 custom-control-label" for="isUnique">
+                                <div class="form-check">
+                                    <input type="checkbox" id="isUnique" name="isUnique" class="me-1 form-check-input"/>
+                                    <label class="mx-2 form-check-label" for="isUnique">
                                         Unique
                                     </label>
                                 </div>
 
-                                <div class="custom-control custom-checkbox">
-                                    <input type="checkbox" name="primaryKey" id="primaryKey" class="mr-1 custom-control-input"/>
-                                    <label class="mx-2 custom-control-label" for="primaryKey">
+                                <div class="form-check">
+                                    <input type="checkbox" name="primaryKey" id="primaryKey" class="me-1 form-check-input"/>
+                                    <label class="mx-2 form-check-label" for="primaryKey">
                                         Primary Key
                                     </label>
                                 </div>
 
-                                <div class="custom-control custom-checkbox">
-                                    <input id="isArray" type="checkbox" name="isArray" class="mr-1 custom-control-input"/>
-                                    <label class="mx-2 custom-control-label" for="isArray">
+                                <div class="form-check">
+                                    <input id="isArray" type="checkbox" name="isArray" class="me-1 form-check-input"/>
+                                    <label class="mx-2 form-check-label" for="isArray">
                                         Array Type
                                     </label>
                                 </div>
@@ -81,11 +80,11 @@
                         </div>
                     </div>
 
-                    <div class="form-group">
+                    <div class="mb-3">
                         {defaultSelector}
                     </div>
 
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Create Column</button>
                     </div>
 
@@ -102,9 +101,9 @@
                         where
                             checkbox tableName = [hsx|
                             <div>
-                                <div class="custom-control custom-checkbox ref" style="display: none;" data-attribute={(singularize tableName) <> "_id"} data-table={tableName} >
-                                    <input id={"checkbox-ref-" <> tableName} type="checkbox" name="isReference" class="mr-1 custom-control-input"/>
-                                    <label for={"checkbox-ref-" <> tableName} class="mx-2 custom-control-label" id="refText">
+                                <div class="form-check ref" style="display: none;" data-attribute={(singularize tableName) <> "_id"} data-table={tableName} >
+                                    <input id={"checkbox-ref-" <> tableName} type="checkbox" name="isReference" class="me-1 form-check-input"/>
+                                    <label for={"checkbox-ref-" <> tableName} class="mx-2 form-check-label" id="refText">
                                         References {tableName}
                                     </label>
                                 </div>
diff --git a/IHP/IDE/SchemaDesigner/View/Columns/NewForeignKey.hs b/IHP/IDE/SchemaDesigner/View/Columns/NewForeignKey.hs
--- a/IHP/IDE/SchemaDesigner/View/Columns/NewForeignKey.hs
+++ b/IHP/IDE/SchemaDesigner/View/Columns/NewForeignKey.hs
@@ -1,7 +1,7 @@
-module IHP.IDE.SchemaDesigner.View.Columns.NewForeignKey where
+module IHP.IDE.SchemaDesigner.View.Columns.NewForeignKey (NewForeignKeyView (..), foreignKeyFormModal) where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
@@ -14,7 +14,7 @@
 
 instance View NewForeignKeyView where
     html NewForeignKeyView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just tableName)}
             {renderColumnSelector tableName  (zip [0..] columns) statements}
         </div>
@@ -22,61 +22,86 @@
         {renderModal modal}
     |]
         where
-            table = findStatementByName tableName statements
-            columns = maybe [] ((.columns) . unsafeGetCreateTable) table
+            columns = getTableColumns tableName statements
+            modal = foreignKeyFormModal
+                (pathTo CreateForeignKeyAction)
+                tableName
+                columnName
+                tableNames
+                (tableName <> "_ref_" <> columnName)
+                Nothing
+                "NoAction"
+                "Add Constraint"
+                "New Foreign Key Constraint"
 
-            modalContent = [hsx|
-                <form method="POST" action={CreateForeignKeyAction}>
-                    <input type="hidden" name="tableName" value={tableName}/>
-                    <input type="hidden" name="columnName" value={columnName}/>
+-- | Shared form modal for creating and editing foreign key constraints.
+foreignKeyFormModal
+    :: (?context :: ControllerContext, ?request :: Request)
+    => Text           -- ^ Form action URL
+    -> Text           -- ^ Table name
+    -> Text           -- ^ Column name
+    -> [Text]         -- ^ Available table names
+    -> Text           -- ^ Constraint name value
+    -> Maybe Text     -- ^ Selected reference table (Nothing = no preselection)
+    -> Text           -- ^ Selected onDelete value
+    -> Text           -- ^ Button text
+    -> Text           -- ^ Modal title
+    -> Modal
+foreignKeyFormModal formAction tableName columnName tableNames constraintNameValue selectedReferenceTable selectedOnDelete buttonText modalTitle =
+    Modal { modalContent, modalFooter, modalCloseUrl, modalTitle }
+    where
+        modalContent = [hsx|
+            <form method="POST" action={formAction}>
+                <input type="hidden" name="tableName" value={tableName}/>
+                <input type="hidden" name="columnName" value={columnName}/>
 
-                    <div class="form-group row">
-                        <label class="col-sm-2 col-form-label">Reference Table:</label>
-                        <div class="col-sm-10">
-                            <select name="referenceTable" class="form-control select2" autofocus="autofocus">
-                                {forEach tableNames renderTableNameSelector}
-                            </select>
-                        </div>
+                <div class="mb-3 row">
+                    <label class="col-sm-2 col-form-label">Reference Table:</label>
+                    <div class="col-sm-10">
+                        <select name="referenceTable" class="form-control select2" autofocus="autofocus">
+                            {forEach tableNames renderTableNameSelector}
+                        </select>
                     </div>
+                </div>
 
-                    <div class="form-group row">
-                        <label class="col-sm-2 col-form-label">Name:</label>
-                        <div class="col-sm-10">
-                            <input name="constraintName" type="text" class="form-control" value={tableName <> "_ref_" <> columnName}/>
-                        </div>
+                <div class="mb-3 row">
+                    <label class="col-sm-2 col-form-label">Name:</label>
+                    <div class="col-sm-10">
+                        <input name="constraintName" type="text" class="form-control" value={constraintNameValue}/>
                     </div>
+                </div>
 
-                    <div class="form-group row">
-                        <label class="col-sm-2 col-form-label">On Delete:</label>
-                        <div class="col-sm-10">
-                            <select name="onDelete" class="form-control select2">
-                                {onDeleteSelector "NoAction"}
-                                {onDeleteSelector "Restrict"}
-                                {onDeleteSelector "SetNull"}
-                                {onDeleteSelector "SetDefault"}
-                                {onDeleteSelector "Cascade"}
-                            </select>
-                        </div>
+                <div class="mb-3 row">
+                    <label class="col-sm-2 col-form-label">On Delete:</label>
+                    <div class="col-sm-10">
+                        <select name="onDelete" class="form-control select2">
+                            {onDeleteSelector "NoAction"}
+                            {onDeleteSelector "Restrict"}
+                            {onDeleteSelector "SetNull"}
+                            {onDeleteSelector "SetDefault"}
+                            {onDeleteSelector "Cascade"}
+                        </select>
                     </div>
+                </div>
 
-                    <div class="text-right">
-                        <button type="submit" class="btn btn-primary">Add Constraint</button>
-                    </div>
-                </form>
-                {select2}
-            |]
-                where
-                    onDeleteSelector option = if option == "NoAction"
-                        then preEscapedToHtml [plain|<option selected>#{option}</option>|]
-                        else preEscapedToHtml [plain|<option>#{option}</option>|]
-                    renderTableNameSelector tableName = [hsx|<option>{tableName}</option>|]
-                    select2 = preEscapedToHtml [plain|
-                        <script>
-                            $('.select2').select2();
-                        </script>
-                    |]
-            modalFooter = mempty 
-            modalCloseUrl = pathTo ShowTableAction { tableName }
-            modalTitle = "New Foreign Key Constraint"
-            modal = Modal { modalContent, modalFooter, modalCloseUrl, modalTitle }
-            
+                <div class="text-end">
+                    <button type="submit" class="btn btn-primary">{buttonText}</button>
+                </div>
+            </form>
+            {select2}
+        |]
+            where
+                renderTableNameSelector name = case selectedReferenceTable of
+                    Just selected | name == selected ->
+                        preEscapedToHtml [plain|<option selected>#{name}</option>|]
+                    _ -> preEscapedToHtml [plain|<option>#{name}</option>|]
+                onDeleteSelector option = if option == selectedOnDelete
+                    then preEscapedToHtml [plain|<option selected>#{option}</option>|]
+                    else preEscapedToHtml [plain|<option>#{option}</option>|]
+                select2 = preEscapedToHtml [plain|
+                    <script>
+                        $('.select2').select2();
+                    </script>
+                |]
+        modalFooter = mempty
+        modalCloseUrl = pathTo ShowTableAction { tableName }
diff --git a/IHP/IDE/SchemaDesigner/View/EnumValues/Edit.hs b/IHP/IDE/SchemaDesigner/View/EnumValues/Edit.hs
--- a/IHP/IDE/SchemaDesigner/View/EnumValues/Edit.hs
+++ b/IHP/IDE/SchemaDesigner/View/EnumValues/Edit.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.EnumValues.Edit where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
@@ -14,7 +14,7 @@
 
 instance View EditEnumValueView where
     html EditEnumValueView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just enumName)}
             {renderEnumSelector enumName (zip [0..] values)}
         </div>
@@ -30,14 +30,14 @@
                     <input type="hidden" name="enumName" value={enumName}/>
                     <input type="hidden" name="valueId" value={tshow valueId}/>
 
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         <label class="col-sm-2 col-form-label">Name:</label>
                         <div class="col-sm-10">
                             <input name="enumValueName" type="text" class="form-control" autofocus="autofocus" value={value}/>
                         </div>
                     </div>
 
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Edit Enum Value</button>
                     </div>
                 </form>
diff --git a/IHP/IDE/SchemaDesigner/View/EnumValues/New.hs b/IHP/IDE/SchemaDesigner/View/EnumValues/New.hs
--- a/IHP/IDE/SchemaDesigner/View/EnumValues/New.hs
+++ b/IHP/IDE/SchemaDesigner/View/EnumValues/New.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.EnumValues.New where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
@@ -12,7 +12,7 @@
 
 instance View NewEnumValueView where
     html NewEnumValueView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just enumName)}
             {renderEnumSelector enumName (zip [0..] values)}
         </div>
@@ -27,7 +27,7 @@
                 <form method="POST" action={CreateEnumValueAction}>
                     <input type="hidden" name="enumName" value={enumName}/>
 
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         <label class="col-sm-2 col-form-label">Name:</label>
                         <div class="col-sm-10">
                             <input name="enumValueName" type="text" class="form-control" autofocus="autofocus"/>
@@ -38,8 +38,8 @@
                         </div>
                     </div>
 
-                    <div class="text-right">
-                        <input type="submit" name="submit" value="Save" class="btn btn-secondary mr-2">
+                    <div class="text-end">
+                        <input type="submit" name="submit" value="Save" class="btn btn-secondary me-2">
                         <input type="submit" name="submit" value="Save & Add Another" class="btn btn-primary">
                     </div>
                 </form>
diff --git a/IHP/IDE/SchemaDesigner/View/Enums/Edit.hs b/IHP/IDE/SchemaDesigner/View/Enums/Edit.hs
--- a/IHP/IDE/SchemaDesigner/View/Enums/Edit.hs
+++ b/IHP/IDE/SchemaDesigner/View/Enums/Edit.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Enums.Edit where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
@@ -13,7 +13,7 @@
 
 instance View EditEnumView where
     html EditEnumView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) Nothing}
             {emptyColumnSelectorContainer}
         </div>
@@ -24,14 +24,14 @@
             modalContent = [hsx|
                 <form method="POST" action={UpdateEnumAction}>
                     <input type="hidden" name="enumId" value={tshow enumId}/>
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         <label class="col-sm-2 col-form-label">Name:</label>
                         <div class="col-sm-10">
                             <input id="nameInput" name="enumName" type="text" class="form-control" autofocus="autofocus" value={enumName}/>
                         </div>
                     </div>
 
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Edit Table</button>
                     </div>
                 </form>
diff --git a/IHP/IDE/SchemaDesigner/View/Enums/New.hs b/IHP/IDE/SchemaDesigner/View/Enums/New.hs
--- a/IHP/IDE/SchemaDesigner/View/Enums/New.hs
+++ b/IHP/IDE/SchemaDesigner/View/Enums/New.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Enums.New where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
@@ -9,7 +9,7 @@
 
 instance View NewEnumView where
     html NewEnumView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) Nothing}
             {emptyColumnSelectorContainer}
         </div>
@@ -20,14 +20,14 @@
             modalContent = [hsx|
                 <form method="POST" action={CreateEnumAction}>
 
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         <label class="col-sm-2 col-form-label">Name:</label>
                         <div class="col-sm-10">
                             <input id="nameInput" name="enumName" type="text" class="form-control" autofocus="autofocus"/>
                         </div>
                     </div>
 
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Create Enum</button>
                     </div>
                 </form>
diff --git a/IHP/IDE/SchemaDesigner/View/Enums/Show.hs b/IHP/IDE/SchemaDesigner/View/Enums/Show.hs
--- a/IHP/IDE/SchemaDesigner/View/Enums/Show.hs
+++ b/IHP/IDE/SchemaDesigner/View/Enums/Show.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Enums.Show where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
 data ShowEnumView = ShowEnumView
@@ -13,7 +13,7 @@
 instance View ShowEnumView where
     html ShowEnumView { .. } = [hsx|
         {renderFlashMessages}
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just name)}
             {renderEnumSelector name (zip [0..] values)}
         </div>
diff --git a/IHP/IDE/SchemaDesigner/View/Indexes/Edit.hs b/IHP/IDE/SchemaDesigner/View/Indexes/Edit.hs
--- a/IHP/IDE/SchemaDesigner/View/Indexes/Edit.hs
+++ b/IHP/IDE/SchemaDesigner/View/Indexes/Edit.hs
@@ -1,8 +1,8 @@
 module IHP.IDE.SchemaDesigner.View.Indexes.Edit where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
-import qualified IHP.IDE.SchemaDesigner.Compiler as SqlCompiler
+import IHP.Postgres.Types
+import qualified IHP.Postgres.Compiler as SqlCompiler
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
@@ -14,7 +14,7 @@
 
 instance View EditIndexView where
     html EditIndexView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just tableName)}
             {renderColumnSelector tableName  (zip [0..] columns) statements}
         </div>
@@ -27,10 +27,7 @@
                     CreateIndex { indexName = name } | name == indexName -> True
                     otherwise -> False
 
-            table :: Maybe Statement
-            table = findStatementByName tableName statements
-
-            columns = maybe [] ((.columns) . unsafeGetCreateTable) table
+            columns = getTableColumns tableName statements
 
             indexColumns = index.columns
                     |> map SqlCompiler.compileIndexColumn
@@ -40,21 +37,21 @@
                 <form method="POST" action={UpdateIndexAction tableName indexName}>
                     <input type="hidden" name="indexName" value={indexName}/>
 
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         <label class="col-sm-2 col-form-label">Name:</label>
                         <div class="col-sm-10">
                             <input name="newIndexName" type="text" class="form-control" value={indexName}/>
                         </div>
                     </div>
                     
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         <label class="col-sm-2 col-form-label">Column:</label>
                         <div class="col-sm-10">
                             <input name="indexColumns" type="text" class="form-control" value={indexColumns}/>
                         </div>
                     </div>
 
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Update Index</button>
                     </div>
                 </form>
diff --git a/IHP/IDE/SchemaDesigner/View/Layout.hs b/IHP/IDE/SchemaDesigner/View/Layout.hs
--- a/IHP/IDE/SchemaDesigner/View/Layout.hs
+++ b/IHP/IDE/SchemaDesigner/View/Layout.hs
@@ -1,6 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Layout
 ( schemaDesignerLayout
 , findStatementByName
+, getTableColumns
 , visualNav
 , renderColumnSelector
 , renderColumn
@@ -16,12 +17,13 @@
 ) where
 
 import IHP.ViewPrelude hiding (primaryKeyColumnNames)
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.ToolServer.Helper.View
 import IHP.IDE.ToolServer.Layout hiding (tableIcon)
-import IHP.IDE.SchemaDesigner.Compiler (compilePostgresType, compileExpression)
+import IHP.Postgres.Compiler (compilePostgresType, compileExpression)
 import qualified Data.List as List
+import IHP.RequestVault.Helper (lookupRequestVault)
 
 schemaDesignerLayout :: Html -> Html
 schemaDesignerLayout inner = toolServerLayout [hsx|
@@ -33,7 +35,7 @@
     </div>
 |]
     where
-        (DatabaseNeedsMigration hasUnmigratedChanges) = fromFrozenContext @DatabaseNeedsMigration
+        (DatabaseNeedsMigration hasUnmigratedChanges) = lookupRequestVault databaseNeedsMigrationVaultKey ?request
 
 unmigratedChanges :: Html
 unmigratedChanges = [hsx|
@@ -53,7 +55,7 @@
             then unmigratedChanges
             else mempty
     where
-        (DatabaseNeedsMigration databaseNeedsMigration) = fromFrozenContext @DatabaseNeedsMigration
+        (DatabaseNeedsMigration databaseNeedsMigration) = lookupRequestVault databaseNeedsMigrationVaultKey ?request
 
         hasPendingMigrations :: Bool
         hasPendingMigrations = False
@@ -63,11 +65,11 @@
         <div id="migration-status-container">
             <div class="alert alert-primary d-flex align-items-center shadow-lg" role="alert">
                 {migrationStatusIcon}
-                <div class="user-select-none">
+                <div class="user-select-none flex-grow-1">
                     <div><strong>Unmigrated Changes</strong></div>
                     Your app database is not in sync with the Schema
                 </div>
-                <div class="ml-auto d-flex justify-content-end">
+                <div class="ms-auto d-flex justify-content-end">
                     <a
                         href={NewMigrationAction}
                         class="btn px-4 btn-dark"
@@ -82,11 +84,11 @@
         <div id="migration-status-container">
             <div class="alert alert-primary d-flex align-items-center shadow-lg" role="alert">
                 {migrationStatusIcon}
-                <div class="user-select-none">
+                <div class="user-select-none flex-grow-1">
                     <div><strong>Pending Changes</strong></div>
                     You have migrations that haven't been run yet
                 </div>
-                <div class="ml-auto d-flex justify-content-end">
+                <div class="ms-auto d-flex justify-content-end">
                     <a
                         href="#"
                         class="btn px-4 btn-dark"
@@ -98,7 +100,7 @@
 
         migrationStatusIcon :: Html
         migrationStatusIcon = preEscapedToHtml [plain|
-            <svg width="33px" height="33px" viewBox="0 0 33 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="mr-3">
+            <svg width="33px" height="33px" viewBox="0 0 33 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="me-3">
                 <g id="Schema" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
                     <g id="Message" transform="translate(-383.000000, -813.000000)" fill="#FFCD1B">
                         <g id="Group" transform="translate(383.000000, 813.000000)">
@@ -114,7 +116,7 @@
 
 databaseControls :: Html
 databaseControls = [hsx|
-<div class="d-flex justify-content-end ml-auto">
+<div class="d-flex justify-content-end ms-auto">
     <form method="POST" action={pathTo UpdateDbAction} id="update-db-form"/>
     <form method="POST" action={pathTo PushToDbAction} id="push-to-db-form"/>
     <form method="POST" action={pathTo DumpDbAction} id="db-to-fixtures-form"/>
@@ -125,18 +127,18 @@
             onclick="checkBeforeUnload()"
             >Migrate DB →</a>
 
-        <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-            <span class="sr-only">Toggle Dropdown</span>
+        <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            <span class="visually-hidden">Toggle Dropdown</span>
         </button>
 
-        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
+        <div class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuLink">
             <button
                 type="submit"
                 form="update-db-form"
                 class="dropdown-item"
-                data-toggle="tooltip"
-                data-placement="left"
-                data-html="true"
+                data-bs-toggle="tooltip"
+                data-bs-placement="left"
+                data-bs-html="true"
                 title="Dumps DB to Fixtures.sql.<br><br>Delete the DB.<br><br>Recreate using Schema.sql and Fixtures.sql"
                 onclick="checkBeforeUnload()"
                 >Update DB</button>
@@ -145,9 +147,9 @@
                 type="submit"
                 class="dropdown-item"
                 form="db-to-fixtures-form"
-                data-toggle="tooltip"
-                data-placement="left"
-                data-html="true"
+                data-bs-toggle="tooltip"
+                data-bs-placement="left"
+                data-bs-html="true"
                 title="Saves the content of all tables to Application/Fixtures.sql"
                 onclick="checkBeforeUnload()"
                 >Save DB to Fixtures</button>
@@ -155,9 +157,9 @@
                 type="submit"
                 class="dropdown-item"
                 form="push-to-db-form"
-                data-toggle="tooltip"
-                data-placement="left"
-                data-html="true"
+                data-bs-toggle="tooltip"
+                data-bs-placement="left"
+                data-bs-html="true"
                 title="Delete the DB and recreate using Application/Schema.sql and Application/Fixture.sql<br><br><strong class=text-danger>Save DB to Fixtures before using this to avoid data loss</strong>"
                 onclick="checkBeforeUnload()"
                 >Push to DB</button>
@@ -174,6 +176,10 @@
         pred CreateEnumType { name } | (toUpper name) == (toUpper (tshow statementName)) = True
         pred _ = False
 
+getTableColumns :: Text -> [Statement] -> [Column]
+getTableColumns tableName statements =
+    maybe [] ((.columns) . unsafeGetCreateTable) (findStatementByName tableName statements)
+
 visualNav :: Html
 visualNav = [hsx|
     <div class="view-selector">
@@ -211,8 +217,8 @@
                 <a
                     href={NewColumnAction tableName}
                     class="btn btn-link btn-add"
-                    data-toggle="tooltip"
-                    data-placement="bottom"
+                    data-bs-toggle="tooltip"
+                    data-bs-placement="bottom"
                     title="Add Column"
                 >{addIcon}</a>
             </div>
@@ -530,9 +536,9 @@
         <div class="toolbox">
             <a
                 href={NewEnumValueAction enumName}
-                class="btn btn-link btn-add mr-1"
-                data-toggle="tooltip"
-                data-placement="bottom"
+                class="btn btn-link btn-add me-1"
+                data-bs-toggle="tooltip"
+                data-bs-placement="bottom"
                 title="Add Table"
                 >{addIcon}</a>
         </div>
@@ -566,14 +572,14 @@
 
 renderObjectSelector statements activeObjectName = [hsx|
     <div class={classes ["col", "object-selector", ("empty", isEmptySelector)]} oncontextmenu="showContextMenu('context-menu-object-root')">
-        <div class="d-flex align-items-center pl-2">
+        <div class="d-flex align-items-center ps-2">
             <h5>Tables</h5>
             <div class="toolbox">
                 <a
                     href={NewTableAction}
-                    class="btn btn-link btn-add mr-1"
-                    data-toggle="tooltip"
-                    data-placement="bottom"
+                    class="btn btn-link btn-add me-1"
+                    data-bs-toggle="tooltip"
+                    data-bs-placement="bottom"
                     title="Add Table"
                     >{addIcon}</a>
             </div>
@@ -603,7 +609,7 @@
             otherwise -> False
 
         enums = whenNonEmpty enumStatements [hsx|
-            <div class="d-flex pl-2">
+            <div class="d-flex ps-2">
                 <h5>Enums</h5>
             </div>
             {forEach enumStatements (\statement -> renderObject (snd statement) (fst statement))}
@@ -611,7 +617,7 @@
 
         renderObject :: Statement -> Int -> Html
         renderObject (StatementCreateTable CreateTable { name }) id = [hsx|
-            <div class={classes [("object object-table w-100 context-table pl-3", True), ("active", Just name == activeObjectName)]} oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
+            <div class={classes [("object object-table w-100 context-table ps-3", True), ("active", Just name == activeObjectName)]} oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
                 <div class="d-flex justify-content-between">
                     <a href={ShowTableAction name} class="flex-grow-1">{name}</a>
 
@@ -637,7 +643,7 @@
                 generateControllerLink = [hsx|<a href={pathTo NewControllerAction <> "?name=" <> name}>Generate Controller</a>|]
                 openControllerLink = [hsx|<a href={pathTo OpenControllerAction <> "?name=" <> name} target="_blank">Open Controller</a>|]
                 controllerDoesNotExist = not $ (ucfirst name) `elem` webControllers
-                (WebControllers webControllers) = fromFrozenContext @WebControllers
+                (WebControllers webControllers) = lookupRequestVault webControllersVaultKey ?request
 
                 rlsEnabled = statements
                         |> map snd
@@ -649,15 +655,15 @@
                 rlsIcon = [hsx|
                         <span
                             class="rls-enabled"
-                            data-toggle="tooltip"
-                            data-placement="right"
-                            data-html="true"
+                            data-bs-toggle="tooltip"
+                            data-bs-placement="right"
+                            data-bs-html="true"
                             title="Row Level Security enabled"
                             >{shieldIcon}</span>
                         |]
 
         renderObject CreateEnumType { name } id = [hsx|
-            <a href={ShowEnumAction name} class={classes [("object object-table w-100 context-enum pl-3", True), ("active", Just name == activeObjectName)]} oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
+            <a href={ShowEnumAction name} class={classes [("object object-table w-100 context-enum ps-3", True), ("active", Just name == activeObjectName)]} oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
                 <div class="d-flex">
                     {name}
                 </div>
@@ -684,6 +690,7 @@
         renderObject UnknownStatement {} id = mempty
         renderObject EnableRowLevelSecurity {} id = mempty
         renderObject CreatePolicy {} id = mempty
+        renderObject _ id = mempty
 
         shouldRenderObject (StatementCreateTable CreateTable {}) = True
         shouldRenderObject CreateEnumType {} = True
diff --git a/IHP/IDE/SchemaDesigner/View/Migrations/Edit.hs b/IHP/IDE/SchemaDesigner/View/Migrations/Edit.hs
--- a/IHP/IDE/SchemaDesigner/View/Migrations/Edit.hs
+++ b/IHP/IDE/SchemaDesigner/View/Migrations/Edit.hs
@@ -22,15 +22,15 @@
 renderForm :: Migration -> Text -> Html
 renderForm migration sqlStatements = [hsx|
 <form method="POST" action={UpdateMigrationAction (migration.revision)}>
-    <div class="form-group" id="form-group-migration_sqlStatements">
+    <div class="mb-3" id="form-group-migration_sqlStatements">
         <textarea name="sqlStatements" placeholder="" id="migration_sqlStatements" class="form-control">{sqlStatements}</textarea>
     </div>
     <div id="migration_sqlStatements_ace"/>
 
     <button
         class="btn btn-primary"
-        data-toggle="tooltip"
-        data-placement="right"
+        data-bs-toggle="tooltip"
+        data-bs-placement="right"
         title="⌘ Enter"
         >Save Migration</button>
 </form>
diff --git a/IHP/IDE/SchemaDesigner/View/Migrations/Index.hs b/IHP/IDE/SchemaDesigner/View/Migrations/Index.hs
--- a/IHP/IDE/SchemaDesigner/View/Migrations/Index.hs
+++ b/IHP/IDE/SchemaDesigner/View/Migrations/Index.hs
@@ -18,12 +18,12 @@
     html IndexView { migrationsWithSql = [] } = emptyState
     html IndexView { .. } = [hsx|
         <div class="pt-3 flex-grow-1" oncontextmenu="showContextMenu('context-menu-migrations')">
-            <div class="d-flex align-items-center mb-3 pr-2">
+            <div class="d-flex align-items-center mb-3 pe-2">
                 <a
                     href={NewMigrationAction}
-                    class="ml-auto btn btn-link btn-add"
-                    data-toggle="tooltip"
-                    data-placement="bottom"
+                    class="ms-auto btn btn-link btn-add"
+                    data-bs-toggle="tooltip"
+                    data-bs-placement="bottom"
                     title="Add Migration"
                     >
                     {addIcon}
@@ -60,16 +60,16 @@
                             <a
                                 href={EditMigrationAction (migration.revision)}
                                 class="btn btn-link btn-add"
-                                data-toggle="tooltip"
-                                data-placement="bottom"
+                                data-bs-toggle="tooltip"
+                                data-bs-placement="bottom"
                                 title="Edit Migration"
                             >{editIcon}</a>
 
                             <a
                                 href={DeleteMigrationAction (migration.revision)}
                                 class="btn btn-link btn-add js-delete"
-                                data-toggle="tooltip"
-                                data-placement="bottom"
+                                data-bs-toggle="tooltip"
+                                data-bs-placement="bottom"
                                 title="Delete Migration"
                             >{deleteIcon}</a>
                         </div>
@@ -77,7 +77,7 @@
 
 
                     currentErrorHtml = unless (isNothing currentError) [hsx|
-                        <div class="text-danger font-weight-bold mr-2">
+                        <div class="text-danger fw-bold me-2">
                             {currentError}
                         </div>
                     |]
@@ -86,7 +86,7 @@
                     runOrStatus =
                         if pending
                                 then [hsx|
-                                <form method="POST" action={RunMigrationAction (migration.revision)} class="mr-2 d-flex align-items-center">
+                                <form method="POST" action={RunMigrationAction (migration.revision)} class="me-2 d-flex align-items-center">
                                     {currentErrorHtml}
                                     <button
                                         class="btn btn-secondary migration-run-button"
@@ -95,7 +95,7 @@
                             |]
                             else [hsx|
                                 <div class="d-flex justify-content-end mb-2">
-                                    <span class="text-muted mr-2 user-select-none" style="opacity: 0.5">{timeAgo revisionTime}</span>
+                                    <span class="text-muted me-2 user-select-none" style="opacity: 0.5">{timeAgo revisionTime}</span>
                                     <strong class="text-success">
                                         {checkmark}
                                     </strong>
diff --git a/IHP/IDE/SchemaDesigner/View/Migrations/New.hs b/IHP/IDE/SchemaDesigner/View/Migrations/New.hs
--- a/IHP/IDE/SchemaDesigner/View/Migrations/New.hs
+++ b/IHP/IDE/SchemaDesigner/View/Migrations/New.hs
@@ -27,7 +27,7 @@
 renderForm :: Text -> Html
 renderForm sqlStatements = [hsx|
 <form method="POST" action={CreateMigrationAction}>
-    <div class="form-group" id="form-group-migration_sqlStatements">
+    <div class="mb-3" id="form-group-migration_sqlStatements">
         <textarea name="sqlStatements" placeholder="" id="migration_sqlStatements" class="form-control">{sqlStatements}</textarea>
     </div>
     <div id="migration_sqlStatements_ace"/>
@@ -37,10 +37,10 @@
             <span id="addMigrationVerb">Run</span> Migration
         </button>
 
-        <div class="ml-3 d-flex align-items-center">
-            <div class="custom-control custom-switch d-flex align-items-center">
-                <input type="checkbox" class="custom-control-input" id="createOnlySwitch" name="createOnly" onchange="addMigrationVerb.innerText = this.checked ? 'Add' : 'Run'"/>
-                <label class="custom-control-label text-muted" for="createOnlySwitch" style="font-size: 14px">Create only, don't run yet</label>
+        <div class="ms-3 d-flex align-items-center">
+            <div class="form-check form-switch d-flex align-items-center">
+                <input type="checkbox" class="form-check-input" id="createOnlySwitch" name="createOnly" onchange="addMigrationVerb.innerText = this.checked ? 'Add' : 'Run'"/>
+                <label class="form-check-label text-muted" for="createOnlySwitch" style="font-size: 14px">Create only, don't run yet</label>
             </div>
         </div>
     </div>
diff --git a/IHP/IDE/SchemaDesigner/View/Policies/Edit.hs b/IHP/IDE/SchemaDesigner/View/Policies/Edit.hs
--- a/IHP/IDE/SchemaDesigner/View/Policies/Edit.hs
+++ b/IHP/IDE/SchemaDesigner/View/Policies/Edit.hs
@@ -1,10 +1,10 @@
 module IHP.IDE.SchemaDesigner.View.Policies.Edit where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
-import qualified IHP.IDE.SchemaDesigner.Compiler as Compiler
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
+import IHP.IDE.SchemaDesigner.View.Policies.New (policyFormModal)
 
 data EditPolicyView = EditPolicyView
     { statements :: [Statement]
@@ -15,7 +15,7 @@
 
 instance View EditPolicyView where
     html EditPolicyView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just tableName)}
             {renderColumnSelector tableName (zip [0..] columns) statements}
         </div>
@@ -23,71 +23,11 @@
         {renderModal modal}
     |]
         where
-            modalContent = [hsx|
-                <form method="POST" action={UpdatePolicyAction} class="edit-policy">
-                    <input type="hidden" name="tableName" value={tableName}/>
-                    <!--
-                        The hidden name field is required as the user could be changing the name and we don't have any other
-                        identifier to refer to the policy besides the name
-                    -->
-                    <input type="hidden" name="name" value={policy.name}/>
-
-                    <!-- These will be filled via JS from the ace editors -->
-                    <input type="hidden" name="using" value={using}/>
-                    <input type="hidden" name="check" value={check}/>
-
-                    <div class="form-group">
-                        <input
-                            id="nameInput"
-                            name="policyName"
-                            type="text"
-                            class="form-control"
-                            autofocus="autofocus"
-                            value={policy.name}
-                            />
-                    </div>
-
-                    <div class="form-group">
-                        <label for="using">Visible if:</label>
-                        <textarea
-                            id="using"
-                            name="using"
-                            type="text"
-                            class="form-control sql-expression"
-                            data-autocomplete-suggestions={autocompleteSuggestions}
-                        >{using}</textarea>
-                        <small class="form-text text-muted">This SQL expression needs to return True if the row should be visible to the current user. This is the <code>USING</code> condition of the Postgres Policy</small>
-                    </div>
-
-                    <div class="form-group">
-                        <label for="using">Additionally, allow INSERT and UPDATE only if:</label>
-                        <textarea
-                            id="check"
-                            name="check"
-                            type="text"
-                            class="form-control sql-expression"
-                            data-autocomplete-suggestions={autocompleteSuggestions}
-                        >{check}</textarea>
-                        <small class="form-text text-muted">Use this to e.g. disallow users changing the user_id to another user's id. This is the <code>CHECK</code> condition of the Postgres Policy</small>
-                    </div>
-
-                    <div class="text-right">
-                        <button type="submit" class="btn btn-primary">Update Policy</button>
-                    </div>
-                </form>
+            extraHiddenFields = [hsx|
+                <!--
+                    The hidden name field is required as the user could be changing the name and we don't have any other
+                    identifier to refer to the policy besides the name
+                -->
+                <input type="hidden" name="name" value={policy.name}/>
             |]
-            modalFooter = mempty
-            modalCloseUrl = pathTo ShowTableAction { tableName }
-            modalTitle = "Edit Policy"
-            modal = Modal { modalContent, modalFooter, modalCloseUrl, modalTitle }
-
-            using = policy.using
-                    |> maybe "" Compiler.compileExpression
-
-            check = policy.check
-                    |> maybe "" Compiler.compileExpression
-
-            autocompleteSuggestions =
-                    columns
-                    |> map (.name)
-                    |> intercalate ","
+            modal = policyFormModal tableName columns policy (pathTo UpdatePolicyAction) extraHiddenFields "Update Policy" "Edit Policy"
diff --git a/IHP/IDE/SchemaDesigner/View/Policies/New.hs b/IHP/IDE/SchemaDesigner/View/Policies/New.hs
--- a/IHP/IDE/SchemaDesigner/View/Policies/New.hs
+++ b/IHP/IDE/SchemaDesigner/View/Policies/New.hs
@@ -1,8 +1,8 @@
-module IHP.IDE.SchemaDesigner.View.Policies.New where
+module IHP.IDE.SchemaDesigner.View.Policies.New (NewPolicyView (..), policyFormModal) where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
-import qualified IHP.IDE.SchemaDesigner.Compiler as Compiler
+import IHP.Postgres.Types
+import qualified IHP.Postgres.Compiler as Compiler
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
@@ -15,7 +15,7 @@
 
 instance View NewPolicyView where
     html NewPolicyView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just tableName)}
             {renderColumnSelector tableName (zip [0..] columns) statements}
         </div>
@@ -23,66 +23,71 @@
         {renderModal modal}
     |]
         where
-            modalContent = [hsx|
-                <form method="POST" action={CreatePolicyAction} class="edit-policy">
-                    <input type="hidden" name="tableName" value={tableName}/>
+            modal = policyFormModal tableName columns policy (pathTo CreatePolicyAction) mempty "Create Policy" "New Policy"
 
-                    <!-- These will be filled via JS from the ace editors -->
-                    <input type="hidden" name="using" value={using}/>
-                    <input type="hidden" name="check" value={check}/>
+-- | Shared form modal for creating and editing policies.
+policyFormModal :: (?context :: ControllerContext, ?request :: Request) => Text -> [Column] -> Statement -> Text -> Html -> Text -> Text -> Modal
+policyFormModal tableName columns policy formAction extraHiddenFields buttonText modalTitle = Modal { modalContent, modalFooter, modalCloseUrl, modalTitle }
+    where
+        modalContent = [hsx|
+            <form method="POST" action={formAction} class="edit-policy">
+                <input type="hidden" name="tableName" value={tableName}/>
+                {extraHiddenFields}
 
-                    <div class="form-group">
-                        <input
-                            id="nameInput"
-                            name="policyName"
-                            type="text"
-                            class="form-control"
-                            autofocus="autofocus"
-                            value={policy.name}
-                            />
-                    </div>
+                <!-- These will be filled via JS from the ace editors -->
+                <input type="hidden" name="using" value={using}/>
+                <input type="hidden" name="check" value={check}/>
 
-                    <div class="form-group">
-                        <label for="using">Visible if:</label>
-                        <textarea
-                            id="using"
-                            name="using"
-                            type="text"
-                            class="form-control sql-expression"
-                            data-autocomplete-suggestions={autocompleteSuggestions}
-                        >{using}</textarea>
-                        <small class="form-text text-muted">This SQL expression needs to return True if the row should be visible to the current user. This is the <code>USING</code> condition of the Postgres Policy</small>
-                    </div>
+                <div class="mb-3">
+                    <input
+                        id="nameInput"
+                        name="policyName"
+                        type="text"
+                        class="form-control"
+                        autofocus="autofocus"
+                        value={policy.name}
+                        />
+                </div>
 
-                    <div class="form-group">
-                        <label for="using">Additionally, allow INSERT and UPDATE only if:</label>
-                        <textarea
-                            id="check"
-                            name="check"
-                            type="text"
-                            class="form-control sql-expression"
-                            data-autocomplete-suggestions={autocompleteSuggestions}
-                        >{check}</textarea>
-                        <small class="form-text text-muted">Use this to e.g. disallow users changing the user_id to another user's id. This is the <code>CHECK</code> condition of the Postgres Policy</small>
-                    </div>
+                <div class="mb-3">
+                    <label for="using">Visible if:</label>
+                    <textarea
+                        id="using"
+                        name="using"
+                        type="text"
+                        class="form-control sql-expression"
+                        data-autocomplete-suggestions={autocompleteSuggestions}
+                    >{using}</textarea>
+                    <small class="form-text">This SQL expression needs to return True if the row should be visible to the current user. This is the <code>USING</code> condition of the Postgres Policy</small>
+                </div>
 
-                    <div class="text-right">
-                        <button type="submit" class="btn btn-primary">Create Policy</button>
-                    </div>
-                </form>
-            |]
-            modalFooter = mempty
-            modalCloseUrl = pathTo ShowTableAction { tableName }
-            modalTitle = "New Policy"
-            modal = Modal { modalContent, modalFooter, modalCloseUrl, modalTitle }
+                <div class="mb-3">
+                    <label for="using">Additionally, allow INSERT and UPDATE only if:</label>
+                    <textarea
+                        id="check"
+                        name="check"
+                        type="text"
+                        class="form-control sql-expression"
+                        data-autocomplete-suggestions={autocompleteSuggestions}
+                    >{check}</textarea>
+                    <small class="form-text">Use this to e.g. disallow users changing the user_id to another user's id. This is the <code>CHECK</code> condition of the Postgres Policy</small>
+                </div>
 
-            using = policy.using
-                    |> maybe "" Compiler.compileExpression
+                <div class="text-end">
+                    <button type="submit" class="btn btn-primary">{buttonText}</button>
+                </div>
+            </form>
+        |]
+        modalFooter = mempty
+        modalCloseUrl = pathTo ShowTableAction { tableName }
 
-            check = policy.check
-                    |> maybe "" Compiler.compileExpression
+        using = policy.using
+                |> maybe "" Compiler.compileExpression
 
-            autocompleteSuggestions =
-                    columns
-                    |> map (.name)
-                    |> intercalate ","
+        check = policy.check
+                |> maybe "" Compiler.compileExpression
+
+        autocompleteSuggestions =
+                columns
+                |> map (.name)
+                |> intercalate ","
diff --git a/IHP/IDE/SchemaDesigner/View/Schema/GeneratedCode.hs b/IHP/IDE/SchemaDesigner/View/Schema/GeneratedCode.hs
--- a/IHP/IDE/SchemaDesigner/View/Schema/GeneratedCode.hs
+++ b/IHP/IDE/SchemaDesigner/View/Schema/GeneratedCode.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Schema.GeneratedCode where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
@@ -14,7 +14,7 @@
     html GeneratedCodeView { .. } = [hsx|
         {visualNav}
         <div class="container">
-            <div class="row no-gutters bg-white">
+            <div class="row g-0 bg-white">
                 {renderObjectSelector (zip [0..] statements) Nothing}
             </div>
         </div>
diff --git a/IHP/IDE/SchemaDesigner/View/Tables/Edit.hs b/IHP/IDE/SchemaDesigner/View/Tables/Edit.hs
--- a/IHP/IDE/SchemaDesigner/View/Tables/Edit.hs
+++ b/IHP/IDE/SchemaDesigner/View/Tables/Edit.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Tables.Edit where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.ToolServer.Routes ()
 import IHP.IDE.SchemaDesigner.View.Layout
@@ -14,7 +14,7 @@
 
 instance View EditTableView where
     html EditTableView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) Nothing}
         </div>
         {renderModal modal}
@@ -23,14 +23,14 @@
             modalContent = [hsx|
                 <form method="POST" action={UpdateTableAction}>
                     <input type="hidden" name="tableId" value={tshow tableId}/>
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         <label class="col-sm-2 col-form-label">Name:</label>
                         <div class="col-sm-10">
                             <input id="nameInput" name="tableName" type="text" class="form-control" autofocus="autofocus" value={tableName}/>
                         </div>
                     </div>
 
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Edit Table</button>
                     </div>
                 </form>
diff --git a/IHP/IDE/SchemaDesigner/View/Tables/Index.hs b/IHP/IDE/SchemaDesigner/View/Tables/Index.hs
--- a/IHP/IDE/SchemaDesigner/View/Tables/Index.hs
+++ b/IHP/IDE/SchemaDesigner/View/Tables/Index.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Tables.Index where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
 data IndexView = IndexView
@@ -12,7 +12,7 @@
     html IndexView { .. } = [hsx|
         {renderFlashMessages}
         {modal}
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) Nothing}
             {emptyColumnSelectorContainer}
         </div>
diff --git a/IHP/IDE/SchemaDesigner/View/Tables/New.hs b/IHP/IDE/SchemaDesigner/View/Tables/New.hs
--- a/IHP/IDE/SchemaDesigner/View/Tables/New.hs
+++ b/IHP/IDE/SchemaDesigner/View/Tables/New.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Tables.New where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.ToolServer.Types
 import IHP.IDE.ToolServer.Routes ()
 import IHP.IDE.SchemaDesigner.View.Layout
@@ -10,7 +10,7 @@
 
 instance View NewTableView where
     html NewTableView { .. } = [hsx|
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) Nothing}
             {emptyColumnSelectorContainer}
         </div>
@@ -21,7 +21,7 @@
             modalContent = [hsx|
                 <form method="POST" action={CreateTableAction}>
 
-                    <div class="form-group row">
+                    <div class="mb-3 row">
                         <label class="col-sm-2 col-form-label">Name:</label>
                         <div class="col-sm-10">
                             <input id="nameInput" name="tableName" type="text" class="form-control" autofocus="autofocus"/>
@@ -31,7 +31,7 @@
                         </div>
                     </div>
 
-                    <div class="text-right">
+                    <div class="text-end">
                         <button type="submit" class="btn btn-primary">Create Table</button>
                     </div>
                 </form>
diff --git a/IHP/IDE/SchemaDesigner/View/Tables/Show.hs b/IHP/IDE/SchemaDesigner/View/Tables/Show.hs
--- a/IHP/IDE/SchemaDesigner/View/Tables/Show.hs
+++ b/IHP/IDE/SchemaDesigner/View/Tables/Show.hs
@@ -1,7 +1,7 @@
 module IHP.IDE.SchemaDesigner.View.Tables.Show where
 
 import IHP.ViewPrelude
-import IHP.IDE.SchemaDesigner.Types
+import IHP.Postgres.Types
 import IHP.IDE.SchemaDesigner.View.Layout
 
 data ShowView = ShowView
@@ -13,7 +13,7 @@
 instance View ShowView where
     html ShowView { .. } = [hsx|
         {renderFlashMessages}
-        <div class="row no-gutters bg-white" id="schema-designer-viewer">
+        <div class="row g-0 bg-white" id="schema-designer-viewer">
             {renderObjectSelector (zip [0..] statements) (Just name)}
             {renderColumnSelector name (zip [0..] columns) statements}
         </div>
diff --git a/IHP/IDE/StatusServer.hs b/IHP/IDE/StatusServer.hs
--- a/IHP/IDE/StatusServer.hs
+++ b/IHP/IDE/StatusServer.hs
@@ -35,14 +35,14 @@
     pure a
 
 runStatusServer ghciIsLoadingVar standardOutput errorOutput clients startMVar stopMVar = do
-
-    let port = ?context.portConfig.appPort |> fromIntegral
+    let appSocket = ?context.appSocket
+    let warpSettings = Warp.defaultSettings
 
     forever do
         _ <- takeMVar startMVar
 
         race_
-            (Warp.run port (waiApp ghciIsLoadingVar clients standardOutput errorOutput))
+            (Warp.runSettingsSocket warpSettings appSocket (waiApp ghciIsLoadingVar clients standardOutput errorOutput))
             (readMVar stopMVar)
 
         isStoppedVar <- takeMVar stopMVar
@@ -202,7 +202,7 @@
                         <a href={("https://github.com/digitallyinduced/ihp/issues/new?body=" :: Text) <> cs (URI.escapeURIString URI.isUnescapedInURI (cs $ ByteString.unlines errorOutput))} target="_blank">Open a GitHub Issue</a>
                     </div>
 
-                    <pre style="font-family: Menlo, monospace; font-size: 10px" id="stdout">{ByteString.unlines (reverse standardOutput)}</pre>
+                    <pre style="font-family: Menlo, monospace; font-size: 10px" id="stdout">{ByteString.unlines (reverse (take 5000 standardOutput))}</pre>
                 </div>
             |]
                 where
diff --git a/IHP/IDE/ToolServer.hs b/IHP/IDE/ToolServer.hs
--- a/IHP/IDE/ToolServer.hs
+++ b/IHP/IDE/ToolServer.hs
@@ -1,4 +1,4 @@
-module IHP.IDE.ToolServer (runToolServer) where
+module IHP.IDE.ToolServer (runToolServer, withToolServerApplication, ToolServerApplicationWithConfig(..)) where
 
 import IHP.Prelude
 import qualified Network.Wai as Wai
@@ -6,13 +6,12 @@
 import IHP.IDE.Types
 import IHP.IDE.PortConfig
 import qualified IHP.ControllerSupport as ControllerSupport
-import IHP.ApplicationContext
 import IHP.ModelSupport
 import IHP.RouterSupport hiding (get)
-import Network.Wai.Session.ClientSession (clientsessionStore)
+import Network.Wai.Session.ClientSession.Deferred (clientsessionStore)
 import qualified Web.ClientSession as ClientSession
 import Network.Wai.Middleware.MethodOverridePost (methodOverridePost)
-import Network.Wai.Session (withSession)
+import Network.Wai.Session.Maybe (withSession)
 import qualified Network.WebSockets as Websocket
 import qualified Network.Wai.Handler.WebSockets as Websocket
 
@@ -34,14 +33,12 @@
 import qualified System.Process as Process
 import System.Info
 import qualified IHP.EnvVar as EnvVar
-import qualified IHP.AutoRefresh.Types as AutoRefresh
 import qualified IHP.AutoRefresh as AutoRefresh
-import IHP.Controller.Context
 import qualified IHP.IDE.ToolServer.Layout as Layout
 import IHP.Controller.Layout
 import qualified IHP.IDE.LiveReloadNotificationServer as LiveReloadNotificationServer
 import qualified IHP.Version as Version
-import qualified IHP.PGListener as PGListener
+import qualified Control.Exception.Safe as Exception
 
 import qualified Network.Wai.Application.Static as Static
 import qualified Network.Wai.Middleware.Approot as Approot
@@ -49,6 +46,12 @@
 import IHP.Controller.NotFound (handleNotFound)
 import IHP.Controller.Session (sessionVaultKey)
 import Paths_ihp_ide (getDataFileName)
+import IHP.RequestVault
+import qualified Data.Vault.Lazy as Vault
+import IHP.Controller.Response (responseHeadersVaultKey)
+import IHP.ControllerSupport (rlsContextVaultKey)
+import Wai.Request.Params.Middleware (requestBodyMiddleware)
+import IHP.Modal.Types (modalContainerVaultKey)
 
 runToolServer :: (?context :: Context) => ToolServerApplication -> _ -> IO ()
 runToolServer toolServerApplication liveReloadClients = do
@@ -59,7 +62,36 @@
 
 startToolServer' :: (?context :: Context) => ToolServerApplication -> Int -> Bool -> _ -> IO ()
 startToolServer' toolServerApplication port isDebugMode liveReloadClients = do
+    withToolServerApplication toolServerApplication port liveReloadClients \weightedApp -> do
+        let openAppUrl = openUrl ("http://localhost:" <> tshow port <> "/")
+        let warpSettings = Warp.defaultSettings
+                |> Warp.setPort port
+                |> Warp.setBeforeMainLoop openAppUrl
 
+        let logMiddleware = if isDebugMode then weightedApp.frameworkConfig.requestLoggerMiddleware else IHP.Prelude.id
+
+        Warp.runSettings warpSettings (logMiddleware weightedApp.application)
+
+-- | Result of building the ToolServer application
+data ToolServerApplicationWithConfig = ToolServerApplicationWithConfig
+    { application :: Wai.Application
+    , frameworkConfig :: Config.FrameworkConfig
+    }
+
+-- | Builds the full ToolServer WAI application with all middlewares applied
+-- and runs the given action with it. The model context is released when the
+-- action completes.
+--
+-- The application includes:
+-- - methodOverridePost (for PUT/DELETE via POST)
+-- - sessionMiddleware (for session handling)
+-- - approotMiddleware (for app root path)
+-- - viewLayoutMiddleware (for setLayout/getLayout support)
+-- - frameworkConfigMiddleware (for framework config in request vault)
+-- - requestBodyMiddleware (for parsing form params - required for controllers)
+-- - websocket support (for live reload)
+withToolServerApplication :: ToolServerApplication -> Int -> _ -> (ToolServerApplicationWithConfig -> IO result) -> IO result
+withToolServerApplication toolServerApplication port liveReloadClients action = do
     frameworkConfig <- Config.buildFrameworkConfig do
         Config.option $ Config.AppHostname "localhost"
         Config.option $ Config.AppPort port
@@ -70,35 +102,56 @@
             Just baseUrl -> Config.option $ Config.BaseUrl baseUrl
             Nothing -> pure ()
 
-    store <- fmap clientsessionStore (ClientSession.getKey "Config/client_session_key.aes")
-    let sessionMiddleware :: Wai.Middleware = withSession store "SESSION" (frameworkConfig.sessionCookie) sessionVaultKey
-    let modelContext = notConnectedModelContext undefined
+    withModelContext frameworkConfig.databaseUrl frameworkConfig.logger \modelContext -> do
+        store <- fmap clientsessionStore (ClientSession.getKey "Config/client_session_key.aes")
+        let sessionMiddleware :: Wai.Middleware = withSession store "SESSION" (frameworkConfig.sessionCookie) sessionVaultKey
 
-    approotMiddleware <- Approot.envFallback
-    
-    PGListener.withPGListener modelContext \pgListener -> do
-        autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener)
+        approotMiddleware <- Approot.envFallbackNamed "IDE_APPROOT"
+
         staticApp <- initStaticApp
 
-        let applicationContext = ApplicationContext { modelContext, autoRefreshServer, frameworkConfig, pgListener }
-        let application :: Wai.Application = \request respond -> do
-                let ?applicationContext = applicationContext
+        let innerApplication :: Wai.Application = \request respond -> do
                 frontControllerToWAIApp @ToolServerApplication @AutoRefresh.AutoRefreshWSApp (\app -> app) toolServerApplication staticApp request respond
 
-        let openAppUrl = openUrl ("http://localhost:" <> tshow port <> "/")
-        let warpSettings = Warp.defaultSettings
-                |> Warp.setPort port
-                |> Warp.setBeforeMainLoop openAppUrl
+        let responseHeadersMiddleware = insertNewIORefVaultMiddleware responseHeadersVaultKey []
+        let rlsContextMiddleware = insertNewIORefVaultMiddleware rlsContextVaultKey Nothing
+        let modalMiddleware = insertNewIORefVaultMiddleware modalContainerVaultKey Nothing
 
-        let logMiddleware = if isDebugMode then frameworkConfig.requestLoggerMiddleware else IHP.Prelude.id
+        let toolServerVaultMiddleware app req respond = do
+                availableApps <- AvailableApps <$> findApplications
+                webControllers <- WebControllers <$> findWebControllers
+                let defaultAppUrl = "http://localhost:" <> tshow toolServerApplication.appPort
+                appUrl <- AppUrl <$> EnvVar.envOrDefault "IHP_BASEURL" defaultAppUrl
+                databaseNeedsMigration <- DatabaseNeedsMigration <$> readIORef toolServerApplication.databaseNeedsMigration
+                hooglePort <- EnvVar.envOrNothing "IHP_HOOGLE_PORT"
+                let hoogleUrl = HoogleUrl $ case hooglePort of
+                        Just port | port /= "" -> Just ("http://localhost:" <> port)
+                        _ -> Nothing
+                let req' = req { Wai.vault = Vault.insert availableAppsVaultKey availableApps
+                                       . Vault.insert webControllersVaultKey webControllers
+                                       . Vault.insert appUrlVaultKey appUrl
+                                       . Vault.insert databaseNeedsMigrationVaultKey databaseNeedsMigration
+                                       . Vault.insert hoogleUrlVaultKey hoogleUrl
+                                       $ req.vault }
+                app req' respond
 
-        Warp.runSettings warpSettings $
-                logMiddleware $ methodOverridePost $ sessionMiddleware $ approotMiddleware
+        let application =
+                methodOverridePost $ sessionMiddleware $ approotMiddleware
+                    $ viewLayoutMiddleware
+                    $ responseHeadersMiddleware
+                    $ rlsContextMiddleware
+                    $ modalMiddleware
+                    $ toolServerVaultMiddleware
+                    $ modelContextMiddleware modelContext
+                    $ frameworkConfigMiddleware frameworkConfig
+                    $ requestBodyMiddleware frameworkConfig.parseRequestBodyOptions
                     $ Websocket.websocketsOr
                         Websocket.defaultConnectionOptions
                         (LiveReloadNotificationServer.app liveReloadClients)
-                        application
+                        innerApplication
 
+        action ToolServerApplicationWithConfig { application, frameworkConfig }
+
 initStaticApp :: IO Wai.Application
 initStaticApp = do
     toolServerStatic <- getDataFileName "static"
@@ -121,6 +174,7 @@
     let defaultOSBrowser = case os of
             "linux" -> "xdg-open"
             "darwin" -> "open"
+            _ -> "xdg-open"
     let browser = selectedBrowser |> fromMaybe defaultOSBrowser
     async $ Process.callCommand (browser <> " " <> cs url)
     pure ()
@@ -143,23 +197,4 @@
 
 instance ControllerSupport.InitControllerContext ToolServerApplication where
     initContext = do
-        availableApps <- AvailableApps <$> findApplications
-        webControllers <- WebControllers <$> findWebControllers
-
-        appPort <- Helper.theAppPort
-        let defaultAppUrl = "http://localhost:" <> tshow appPort
-        appUrl :: Text <- EnvVar.envOrDefault "IHP_BASEURL" defaultAppUrl
-
-        putContext availableApps
-        putContext webControllers
-        putContext (AppUrl appUrl)
         setLayout Layout.toolServerLayout
-
-        databaseNeedsMigration <- readDatabaseNeedsMigration
-        putContext (DatabaseNeedsMigration databaseNeedsMigration)
-
-
-readDatabaseNeedsMigration :: (?context :: ControllerContext) => IO Bool
-readDatabaseNeedsMigration = do
-    context <- fromContext @ToolServerApplication
-    readIORef context.databaseNeedsMigration
diff --git a/IHP/IDE/ToolServer/Helper/Controller.hs b/IHP/IDE/ToolServer/Helper/Controller.hs
--- a/IHP/IDE/ToolServer/Helper/Controller.hs
+++ b/IHP/IDE/ToolServer/Helper/Controller.hs
@@ -17,18 +17,16 @@
 import IHP.Prelude
 import IHP.ControllerSupport
 import IHP.IDE.ToolServer.Types
-import qualified IHP.IDE.PortConfig as PortConfig
-import IHP.IDE.Types
 import qualified Network.Socket as Socket
 import qualified System.Process as Process
 import System.Info (os)
 import qualified IHP.EnvVar as EnvVar
 import IHP.Controller.Context
-import System.IO.Unsafe (unsafePerformIO)
 
 import qualified Data.Text as Text
-import System.Directory
+import qualified System.Directory.OsPath as Directory
 import qualified Data.Text.IO as IO
+import System.OsPath (encodeUtf, decodeUtf)
 
 -- | Returns the port used by the running app. Usually returns @8000@.
 theAppPort :: (?context :: ControllerContext) => IO Socket.PortNumber
@@ -61,27 +59,41 @@
         [] -> case os of
             "linux" -> (False, "xdg-open")
             "darwin" -> (False, "open")
+            _ -> (False, "xdg-open")
 
 
 findWebControllers :: IO [Text]
 findWebControllers = do
-    directoryFiles <-  listDirectory "Web/Controller"
-    let controllerFiles :: [Text] =  filter (\x -> not $ "Prelude" `isInfixOf` x || "Context" `isInfixOf` x)  $ map cs directoryFiles
-    pure $ map (Text.replace ".hs" "") controllerFiles
+    osPath <- encodeUtf "Web/Controller"
+    exists <- Directory.doesDirectoryExist osPath
+    if exists
+        then do
+            osEntries <- Directory.listDirectory osPath
+            directoryFiles <- mapM decodeUtf osEntries
+            let controllerFiles :: [Text] =  filter (\x -> not $ "Prelude" `isInfixOf` x || "Context" `isInfixOf` x)  $ map cs directoryFiles
+            pure $ map (Text.replace ".hs" "") controllerFiles
+        else pure []
 
 findControllers :: Text -> IO [Text]
 findControllers application = do
-    directoryFiles <-  listDirectory $ cs $ application <> "/Controller"
+    osPath <- encodeUtf (cs $ application <> "/Controller")
+    osEntries <- Directory.listDirectory osPath
+    directoryFiles <- mapM decodeUtf osEntries
     let controllerFiles :: [Text] =  filter (\x -> not $ "Prelude" `isInfixOf` x || "Context" `isInfixOf` x)  $ map cs directoryFiles
     pure $ map (Text.replace ".hs" "") controllerFiles
 
 findApplications :: IO ([Text])
 findApplications = do
-    mainhs <- IO.readFile "Main.hs"
-    let imports = filter (\line -> "import " `isPrefixOf` line && ".FrontController" `isSuffixOf` line) (lines mainhs)
-    pure (map removeImport imports)
-        where
-            removeImport line = Text.replace ".FrontController" "" (Text.replace "import " "" line)
+    osPath <- encodeUtf "Main.hs"
+    exists <- Directory.doesFileExist osPath
+    if exists
+        then do
+            mainhs <- IO.readFile "Main.hs"
+            let imports = filter (\line -> "import " `isPrefixOf` line && ".FrontController" `isSuffixOf` line) (lines mainhs)
+            pure (map removeImport imports)
+        else pure []
+    where
+        removeImport line = Text.replace ".FrontController" "" (Text.replace "import " "" line)
 
 theToolServerApplication :: (?context :: ControllerContext) => IO ToolServerApplication
 theToolServerApplication = fromContext @ToolServerApplication
diff --git a/IHP/IDE/ToolServer/Helper/View.hs b/IHP/IDE/ToolServer/Helper/View.hs
--- a/IHP/IDE/ToolServer/Helper/View.hs
+++ b/IHP/IDE/ToolServer/Helper/View.hs
@@ -57,6 +57,17 @@
 |]
 
 
+searchIcon :: Html
+searchIcon = preEscapedToHtml [plain|
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g fill="#E5EAEB">
+            <path d="M8,2 C11.3137,2 14,4.68629 14,8 C14,9.29583 13.5892,10.4957 12.8907,11.4765 L17.7071,16.2929 C18.0976,16.6834 18.0976,17.3166 17.7071,17.7071 C17.3166,18.0976 16.6834,18.0976 16.2929,17.7071 L11.4765,12.8907 C10.4957,13.5892 9.29583,14 8,14 C4.68629,14 2,11.3137 2,8 C2,4.68629 4.68629,2 8,2 Z M8,4 C5.79086,4 4,5.79086 4,8 C4,10.2091 5.79086,12 8,12 C10.2091,12 12,10.2091 12,8 C12,5.79086 10.2091,4 8,4 Z"></path>
+        </g>
+    </g>
+</svg>
+|]
+
 startIcon :: Html
 startIcon = preEscapedToHtml [plain|
 <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" fill="currentColor"><g><rect fill="none" height="24" width="24"/></g><g><path d="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M12,20c-4.41,0-8-3.59-8-8s3.59-8,8-8s8,3.59,8,8 S16.41,20,12,20z M9.5,16.5l7-4.5l-7-4.5V16.5z"/></g></svg>
diff --git a/IHP/IDE/ToolServer/Layout.hs b/IHP/IDE/ToolServer/Layout.hs
--- a/IHP/IDE/ToolServer/Layout.hs
+++ b/IHP/IDE/ToolServer/Layout.hs
@@ -5,6 +5,9 @@
 import IHP.IDE.ToolServer.Routes ()
 import qualified IHP.Version as Version
 import IHP.IDE.ToolServer.Helper.View
+import System.IO.Unsafe (unsafePerformIO)
+import IHP.RequestVault.Helper (lookupRequestVault)
+import IHP.IDE.CodeGen.Types (defaultUuidFunction)
 
 toolServerLayout :: Html -> Html
 toolServerLayout inner = [hsx|
@@ -20,9 +23,9 @@
         <link rel="stylesheet" href={assetPath "/vendor/select2.min.css"}/>
 
         <script src={assetPath "/vendor/morphdom-umd.min.js"}></script>
-        <script src={assetPath "/vendor/jquery-3.6.0.min.js"}></script>
+        <script src={assetPath "/vendor/jquery-4.0.0.min.js"}></script>
         <script src={assetPath "/vendor/timeago.js"}></script>
-        <script src={assetPath "/vendor/popper.min.js"}></script>
+        <script src={assetPath "/vendor/popper-2.11.6.min.js"}></script>
         <script src={assetPath "/vendor/bootstrap.min.js"}></script>
         
 
@@ -49,7 +52,7 @@
 
         <title>IHP IDE</title>
     </head>
-    <body class="d-flex h-100 flex-row">
+    <body class="d-flex h-100 flex-row" data-default-uuid-function={defaultUuidFn <> "()"}>
         <div id="nav">
             <img id="nav-logo" src="/ihp-icon.svg" alt="IHP: Integrated Haskell Platform">
             <div id="ihp-plan">{ihpEditionTitle}</div>
@@ -59,6 +62,7 @@
             {codegen}
             {logs}
             {docu}
+            {hpiHoogle}
 
             {when isBasicEdition getPro}
             {help}
@@ -70,7 +74,10 @@
     </body>
 </html>
 |]  where
-        (AvailableApps appNames) = fromFrozenContext @AvailableApps
+        defaultUuidFn :: Text
+        defaultUuidFn = unsafePerformIO defaultUuidFunction
+        {-# NOINLINE defaultUuidFn #-}
+        (AvailableApps appNames) = lookupRequestVault availableAppsVaultKey ?request
         apps = forEach appNames appNavItem
         schema = navItem "SCHEMA" schemaIcon (pathTo TablesAction) (isSchemaEditorController)
         data_ = navItem "DATA" dataIcon (pathTo ShowDatabaseAction) (isActiveController @DataController)
@@ -79,7 +86,12 @@
         logs = navItem "LOGS" serverIcon (pathTo AppLogsAction) (isActiveController @LogsController)
         lint = navItem "LINT" flagIcon "#" False
         docu = navItem "DOCS" docsIcon "https://ihp.digitallyinduced.com/Guide/" False
-        
+
+        (HoogleUrl hoogleUrl) = lookupRequestVault hoogleUrlVaultKey ?request
+        hpiHoogle = case hoogleUrl of
+            Just url -> navItem "HOOGLE" searchIcon url False
+            Nothing -> mempty
+
         isSchemaEditorController =
                     (  isActiveController @SchemaController
                     || isActiveController @TablesController
@@ -93,11 +105,12 @@
             <a
                 href="#"
                 class="nav-item"
-                data-container="body"
-                data-toggle="popover"
-                data-placement="right"
-                data-title="Questions, or need help with Haskell type errors?"
-                data-trigger="focus"
+                data-bs-container="body"
+                data-bs-toggle="popover"
+                data-bs-placement="right"
+                data-bs-title="Questions, or need help with Haskell type errors?"
+                data-bs-trigger="focus"
+                tabindex="0"
                 id="nav-help"
             >
                 {helpIcon}
@@ -153,8 +166,8 @@
                 target :: Maybe Text
                 target = if isExternal then "_blank" else Nothing
 
-appUrl :: (?context :: ControllerContext) => Text
-appUrl = let (AppUrl url) = fromFrozenContext @AppUrl in url
+appUrl :: (?context :: ControllerContext, ?request :: Request) => Text
+appUrl = let (AppUrl url) = lookupRequestVault appUrlVaultKey ?request in url
 
 -- | https://github.com/encharm/Font-Awesome-SVG-PNG/blob/master/white/svg/terminal.svg
 terminalIcon = preEscapedToHtml [plain|<svg viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M649 983l-466 466q-10 10-23 10t-23-10l-50-50q-10-10-10-23t10-23l393-393-393-393q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l466 466q10 10 10 23t-10 23zm1079 457v64q0 14-9 23t-23 9h-960q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h960q14 0 23 9t9 23z" fill="#fff"/></svg>|]
diff --git a/IHP/IDE/ToolServer/Types.hs b/IHP/IDE/ToolServer/Types.hs
--- a/IHP/IDE/ToolServer/Types.hs
+++ b/IHP/IDE/ToolServer/Types.hs
@@ -1,15 +1,12 @@
 module IHP.IDE.ToolServer.Types where
 
 import IHP.Prelude
-import qualified IHP.IDE.Types as DevServer
-import Control.Concurrent.MVar
-import qualified Data.ByteString.Builder as ByteString
 import Network.Socket (PortNumber)
+import qualified Data.Vault.Lazy as Vault
+import System.IO.Unsafe (unsafePerformIO)
 
 data ToolServerApplication = ToolServerApplication
-        { postgresStandardOutput :: !(IORef ByteString.Builder)
-        , postgresErrorOutput :: !(IORef ByteString.Builder)
-        , appStandardOutput :: !(IORef [ByteString])
+        { appStandardOutput :: !(IORef [ByteString])
         , appErrorOutput :: !(IORef [ByteString])
         , appPort :: !PortNumber
         , databaseNeedsMigration :: !(IORef Bool)
@@ -94,6 +91,7 @@
 data LogsController
     = AppLogsAction
     | PostgresLogsAction
+    | ServiceLogsAction { serviceName :: Text }
     | OpenEditorAction
     deriving (Eq, Show, Data)
 
@@ -162,6 +160,37 @@
 
 newtype DatabaseNeedsMigration = DatabaseNeedsMigration Bool
 
+-- | Wrapper to pass the Hoogle URL to the layout.
+-- Contains Nothing when Hoogle is not enabled.
+newtype HoogleUrl = HoogleUrl (Maybe Text)
+
+availableAppsVaultKey :: Vault.Key AvailableApps
+availableAppsVaultKey = unsafePerformIO Vault.newKey
+{-# NOINLINE availableAppsVaultKey #-}
+
+webControllersVaultKey :: Vault.Key WebControllers
+webControllersVaultKey = unsafePerformIO Vault.newKey
+{-# NOINLINE webControllersVaultKey #-}
+
+appUrlVaultKey :: Vault.Key AppUrl
+appUrlVaultKey = unsafePerformIO Vault.newKey
+{-# NOINLINE appUrlVaultKey #-}
+
+databaseNeedsMigrationVaultKey :: Vault.Key DatabaseNeedsMigration
+databaseNeedsMigrationVaultKey = unsafePerformIO Vault.newKey
+{-# NOINLINE databaseNeedsMigrationVaultKey #-}
+
+hoogleUrlVaultKey :: Vault.Key HoogleUrl
+hoogleUrlVaultKey = unsafePerformIO Vault.newKey
+{-# NOINLINE hoogleUrlVaultKey #-}
+
 data SqlConsoleResult
     = SelectQueryResult ![[DynamicField]]
     | InsertOrUpdateResult !Int64
+
+data SqlConsoleError = SqlConsoleError
+    { errorMessage :: Text
+    , errorDetail :: Text
+    , errorHint :: Text
+    , errorState :: Text
+    }
diff --git a/IHP/IDE/Types.hs b/IHP/IDE/Types.hs
--- a/IHP/IDE/Types.hs
+++ b/IHP/IDE/Types.hs
@@ -1,7 +1,6 @@
 module IHP.IDE.Types where
 
 import ClassyPrelude
-import System.Process.Internals
 import qualified System.Process as Process
 import qualified GHC.IO.Handle as Handle
 import qualified Network.WebSockets as Websocket
@@ -11,14 +10,16 @@
 import Data.UUID
 import qualified IHP.Log.Types as Log
 import qualified IHP.Log as Log
-import qualified Data.ByteString.Builder as ByteString
 import qualified Control.Concurrent.Chan.Unagi as Queue
+import qualified Network.Socket as Socket
+import System.OsPath (OsPath, decodeUtf)
 
-procDirenvAware :: (?context :: Context) => FilePath -> [String] -> Process.CreateProcess
-procDirenvAware command args =
-    if ?context.wrapWithDirenv
-        then Process.proc "direnv" (["exec", ".", command] <> args)
-        else Process.proc command args
+procDirenvAware :: (?context :: Context) => OsPath -> [String] -> IO Process.CreateProcess
+procDirenvAware command args = do
+    commandStr <- decodeUtf command
+    pure if ?context.wrapWithDirenv
+        then Process.proc "direnv" (["exec", ".", commandStr] <> args)
+        else Process.proc commandStr args
 
 sendGhciCommand :: (?context :: Context) => Handle -> ByteString -> IO ()
 sendGhciCommand inputHandle command = do
@@ -41,4 +42,5 @@
     , liveReloadClients :: !(IORef (Map UUID Websocket.Connection))
     , wrapWithDirenv :: !Bool
     , lastSchemaCompilerError :: !(IORef (Maybe SomeException))
+    , appSocket :: !Socket.Socket -- ^ Shared socket for seamless app restarts
     }
diff --git a/IHP/SchemaCompiler.hs b/IHP/SchemaCompiler.hs
deleted file mode 100644
--- a/IHP/SchemaCompiler.hs
+++ /dev/null
@@ -1,976 +0,0 @@
-module IHP.SchemaCompiler
-( compile
-, compileStatementPreview
-) where
-
-import ClassyPrelude
-import Data.String.Conversions (cs)
-import "interpolate" Data.String.Interpolate (i)
-import IHP.NameSupport (tableNameToModelName, columnNameToFieldName, enumValueToControllerName)
-import qualified Data.Text as Text
-import qualified System.Directory as Directory
-import Data.List.Split
-import IHP.HaskellSupport
-import qualified IHP.IDE.SchemaDesigner.Parser as SchemaDesigner
-import IHP.IDE.SchemaDesigner.Types
-import qualified IHP.IDE.SchemaDesigner.Compiler as SqlCompiler
-import qualified Control.Exception as Exception
-import NeatInterpolation
-import Text.Countable (pluralize)
-
-data CompileException = CompileException ByteString deriving (Show)
-instance Exception CompileException where
-    displayException (CompileException message) = cs message
-
-compile :: IO ()
-compile = do
-    let options = fullCompileOptions
-    SchemaDesigner.parseSchemaSql >>= \case
-        Left parserError -> Exception.throwIO (CompileException parserError)
-        Right statements -> do
-            -- let validationErrors = validate database
-            -- unless (null validationErrors) (error $ "Schema.hs contains errors: " <> cs (unsafeHead validationErrors))
-            Directory.createDirectoryIfMissing True "build/Generated"
-
-            forEach (compileModules options (Schema statements)) \(path, body) -> do
-                    writeIfDifferent path body
-
-compileModules :: CompilerOptions -> Schema -> [(FilePath, Text)]
-compileModules options schema =
-    [ ("build/Generated/Enums.hs", compileEnums options schema)
-    , ("build/Generated/ActualTypes.hs", compileTypes options schema)
-    ] <> tableModules options schema <>
-    [ ("build/Generated/Types.hs", compileIndex schema)
-    ]
-
-applyTables :: (CreateTable -> (FilePath, Text)) -> Schema -> [(FilePath, Text)]
-applyTables applyFunction schema =
-    let ?schema = schema
-    in
-        schema.statements
-        |> mapMaybe (\case
-                StatementCreateTable table | tableHasPrimaryKey table -> Just (applyFunction table)
-                otherwise -> Nothing
-            )
-
-tableModules :: CompilerOptions -> Schema -> [(FilePath, Text)]
-tableModules options schema =
-    let ?schema = schema
-    in
-        applyTables (tableModule options) schema
-        <> applyTables tableIncludeModule schema
-
-tableModule :: (?schema :: Schema) => CompilerOptions -> CreateTable -> (FilePath, Text)
-tableModule options table =
-        ("build/Generated/" <> cs (tableNameToModelName table.name) <> ".hs", body)
-    where
-        body = Text.unlines
-            [ prelude
-            , tableModuleBody options table
-            ]
-        moduleName = "Generated." <> tableNameToModelName table.name
-        prelude = [trimming|
-            -- This file is auto generated and will be overriden regulary. Please edit `Application/Schema.sql` to change the Types\n"
-            {-# LANGUAGE TypeSynonymInstances, FlexibleInstances, InstanceSigs, MultiParamTypeClasses, TypeFamilies, DataKinds, TypeOperators, UndecidableInstances, ConstraintKinds, StandaloneDeriving  #-}
-            {-# OPTIONS_GHC -Wno-unused-imports -Wno-dodgy-imports -Wno-unused-matches #-}
-            module $moduleName where
-            $defaultImports
-            import Generated.ActualTypes
-        |]
-
-tableIncludeModule :: (?schema :: Schema) => CreateTable -> (FilePath, Text)
-tableIncludeModule table =
-        ("build/Generated/" <> cs (tableNameToModelName table.name) <> "Include.hs", prelude <> compileInclude table)
-    where
-        moduleName = "Generated." <> tableNameToModelName table.name <> "Include"
-        prelude = [trimming|
-            -- This file is auto generated and will be overriden regulary. Please edit `Application/Schema.sql` to change the Types\n"
-            module $moduleName where
-            import Generated.ActualTypes
-            import IHP.ModelSupport (Include, GetModelById)
-        |] <> "\n\n"
-
-
-tableModuleBody :: (?schema :: Schema) => CompilerOptions -> CreateTable -> Text
-tableModuleBody options table = Text.unlines
-    [ compileInputValueInstance table
-    , compileFromRowInstance table
-    , compileGetModelName table
-    , compileCreate table
-    , compileUpdate table
-    , compileBuild table
-    , compileFilterPrimaryKeyInstance table
-    , if needsHasFieldId table
-            then compileHasFieldId table
-            else ""
-    , if options.compileGetAndSetFieldInstances
-            then compileSetFieldInstances table <> compileUpdateFieldInstances table
-            else ""
-    ]
-
-newtype Schema = Schema { statements :: [Statement] }
-
-data CompilerOptions = CompilerOptions {
-        -- | We can toggle the generation of @SetField@ and @GetField@ instances.
-        -- This is e.g. disabled when showing the code preview in the schema designer
-        -- as it's very noisy and does not add any values. But of course it's needed
-        -- when do a compilation for the Types.hs
-        compileGetAndSetFieldInstances :: Bool
-    }
-
-fullCompileOptions :: CompilerOptions
-fullCompileOptions = CompilerOptions { compileGetAndSetFieldInstances = True }
-
-previewCompilerOptions :: CompilerOptions
-previewCompilerOptions = CompilerOptions { compileGetAndSetFieldInstances = False }
-
-atomicType :: PostgresType -> Text
-atomicType = \case
-    PSmallInt -> "Int"
-    PInt -> "Int"
-    PBigInt -> "Integer"
-    PJSONB -> "Data.Aeson.Value"
-    PText -> "Text"
-    PBoolean   -> "Bool"
-    PTimestampWithTimezone -> "UTCTime"
-    PUUID -> "UUID"
-    PSerial -> "Int"
-    PBigserial -> "Integer"
-    PReal -> "Float"
-    PDouble -> "Double"
-    PDate -> "Data.Time.Calendar.Day"
-    PBinary -> "(Binary ByteString)"
-    PTime -> "TimeOfDay"
-    (PInterval _) -> "PGInterval"
-    PCustomType theType -> tableNameToModelName theType
-    PTimestamp -> "LocalTime"
-    (PNumeric _ _) -> "Scientific"
-    (PVaryingN _) -> "Text"
-    (PCharacterN _) -> "Text"
-    PArray type_ -> "[" <> atomicType type_ <> "]"
-    PPoint -> "Point"
-    PPolygon -> "Polygon"
-    PInet -> "Net.IP.IP"
-    PTSVector -> "TSVector"
-
-haskellType :: (?schema :: Schema) => CreateTable -> Column -> Text
-haskellType table@CreateTable { name = tableName, primaryKeyConstraint } column@Column { name, columnType, notNull, generator }
-    | [name] == primaryKeyColumnNames primaryKeyConstraint = "(" <> primaryKeyTypeName tableName <> ")"
-    | otherwise =
-        let
-            actualType =
-                case findForeignKeyConstraint table column of
-                    Just (ForeignKeyConstraint { referenceTable }) -> "(" <> primaryKeyTypeName referenceTable <> ")"
-                    _ -> atomicType columnType
-        in
-            if not notNull || isJust generator
-                then "(Maybe " <> actualType <> ")"
-                else actualType
--- haskellType table (HasMany {name}) = "(QueryBuilder.QueryBuilder " <> tableNameToModelName name <> ")"
-
-
-writeIfDifferent :: FilePath -> Text -> IO ()
-writeIfDifferent path content = do
-    alreadyExists <- Directory.doesFileExist path
-    existingContent <- if alreadyExists then readFile path else pure ""
-    when (existingContent /= cs content) do
-        putStrLn $ "Updating " <> cs path
-        writeFile (cs path) (cs content)
-
-compileTypes :: CompilerOptions -> Schema -> Text
-compileTypes options schema@(Schema statements) = Text.unlines
-        [ prelude
-        , let ?schema = schema in body
-        ]
-    where
-        body :: (?schema :: Schema) => Text
-        body =
-            statements
-                |> mapMaybe (\case
-                    StatementCreateTable table | tableHasPrimaryKey table -> Just (compileActualTypesForTable table)
-                    otherwise -> Nothing
-                )
-                |> Text.intercalate "\n\n"
-        prelude = [trimming|
-            -- This file is auto generated and will be overriden regulary. Please edit `Application/Schema.sql` to change the Types\n"
-            {-# LANGUAGE TypeSynonymInstances, FlexibleInstances, InstanceSigs, MultiParamTypeClasses, TypeFamilies, DataKinds, TypeOperators, UndecidableInstances, ConstraintKinds, StandaloneDeriving  #-}
-            {-# OPTIONS_GHC -Wno-unused-imports -Wno-dodgy-imports -Wno-unused-matches #-}
-            module Generated.ActualTypes (module Generated.ActualTypes, module Generated.Enums) where
-            $defaultImports
-            import Generated.Enums
-        |]
-
-compileActualTypesForTable :: (?schema :: Schema) => CreateTable -> Text
-compileActualTypesForTable table = Text.unlines
-    [ compileData table
-    , compilePrimaryKeyInstance table
-    , compileTypeAlias table
-    , compileHasTableNameInstance table
-    , compileDefaultIdInstance table
-    , compileTableInstance table
-    ]
-
-compileIndex :: Schema -> Text
-compileIndex schema = [trimming|
-        -- This file is auto generated and will be overriden regulary. Please edit `Application/Schema.sql` to change the Types\n"
-        module Generated.Types ($rexports) where
-        import Generated.ActualTypes
-        $tableModuleImports
-    |]
-        where
-            tableModuleNames =
-                schema.statements
-                |> map (\case
-                        StatementCreateTable table ->
-                            let modelName = tableNameToModelName table.name
-                            in
-                                [ "Generated." <> modelName
-                                , "Generated." <> modelName <> "Include"
-                                ]
-                        otherwise -> []
-                    )
-                |> concat
-            tableModuleImports = tableModuleNames
-                    |> map (\name -> "import " <> name)
-                    |> Text.unlines
-
-            rexportedModules = ["Generated.ActualTypes"] <> tableModuleNames
-
-            rexports = rexportedModules
-                    |> map (\moduleName -> "module " <> moduleName)
-                    |> Text.intercalate ", "
-
-
-defaultImports = [trimming|
-    import IHP.HaskellSupport
-    import IHP.ModelSupport
-    import CorePrelude hiding (id)
-    import Data.Time.Clock
-    import Data.Time.LocalTime
-    import qualified Data.Time.Calendar
-    import qualified Data.List as List
-    import qualified Data.ByteString as ByteString
-    import qualified Net.IP
-    import Database.PostgreSQL.Simple
-    import Database.PostgreSQL.Simple.FromRow
-    import Database.PostgreSQL.Simple.FromField hiding (Field, name)
-    import Database.PostgreSQL.Simple.ToField hiding (Field)
-    import qualified IHP.Controller.Param
-    import GHC.TypeLits
-    import Data.UUID (UUID)
-    import Data.Default
-    import qualified IHP.QueryBuilder as QueryBuilder
-    import qualified Data.Proxy
-    import GHC.Records
-    import Data.Data
-    import qualified Data.String.Conversions
-    import qualified Data.Text.Encoding
-    import qualified Data.Aeson
-    import Database.PostgreSQL.Simple.Types (Query (Query), Binary ( .. ))
-    import qualified Database.PostgreSQL.Simple.Types
-    import IHP.Job.Types
-    import IHP.Job.Queue ()
-    import qualified Control.DeepSeq as DeepSeq
-    import qualified Data.Dynamic
-    import Data.Scientific
-|]
-
-
-
-compileEnums :: CompilerOptions -> Schema -> Text
-compileEnums options schema@(Schema statements) = Text.unlines
-        [ prelude
-        , let ?schema = schema
-          in intercalate "\n\n" (mapMaybe compileStatement statements)
-        ]
-    where
-        compileStatement enum@(CreateEnumType {}) = Just (compileEnumDataDefinitions enum)
-        compileStatement _ = Nothing
-        prelude = [trimming|
-            -- This file is auto generated and will be overriden regulary. Please edit `Application/Schema.sql` to change the Types\n"
-            module Generated.Enums where
-            import CorePrelude
-            import IHP.ModelSupport
-            import Database.PostgreSQL.Simple
-            import Database.PostgreSQL.Simple.FromField hiding (Field, name)
-            import Database.PostgreSQL.Simple.ToField hiding (Field)
-            import qualified IHP.Controller.Param
-            import Data.Default
-            import qualified IHP.QueryBuilder as QueryBuilder
-            import qualified Data.String.Conversions
-            import qualified Data.Text.Encoding
-            import qualified Control.DeepSeq as DeepSeq
-        |]
-
-compileStatementPreview :: [Statement] -> Statement -> Text
-compileStatementPreview statements statement =
-    let ?schema = Schema statements
-    in
-        case statement of
-            CreateEnumType {} -> compileEnumDataDefinitions statement
-            StatementCreateTable table -> Text.unlines
-                [ compileActualTypesForTable table
-                , tableModuleBody previewCompilerOptions table
-                ]
-
--- | Skip generation of tables with no primary keys
-tableHasPrimaryKey :: CreateTable -> Bool
-tableHasPrimaryKey table = table.primaryKeyConstraint /= (PrimaryKeyConstraint [])
-
-compileTypeAlias :: (?schema :: Schema) => CreateTable -> Text
-compileTypeAlias table@(CreateTable { name, columns }) =
-        "type "
-        <> modelName
-        <> " = "
-        <> modelName
-        <> "' "
-        <> unwords (map (haskellType table) (variableAttributes table))
-        <> hasManyDefaults
-        <> "\n"
-    where
-        modelName = tableNameToModelName name
-        hasManyDefaults = columnsReferencingTable name
-                |> map (\(tableName, columnName) -> "(QueryBuilder.QueryBuilder \"" <> tableName <> "\")")
-                |> unwords
-
-primaryKeyTypeName :: Text -> Text
-primaryKeyTypeName name = "Id' " <> tshow name <> ""
-
-compileData :: (?schema :: Schema) => CreateTable -> Text
-compileData table@(CreateTable { name, columns }) =
-        "data " <> modelName <> "' " <> typeArguments
-        <> " = " <> modelName <> " {"
-        <>
-            table
-            |> dataFields
-            |> map (\(fieldName, fieldType) -> fieldName <> " :: " <> fieldType)
-            |> commaSep
-        <> "} deriving (Eq, Show)\n"
-    where
-        modelName = tableNameToModelName name
-        typeArguments :: Text
-        typeArguments = dataTypeArguments table |> unwords
-
-compileInputValueInstance :: CreateTable -> Text
-compileInputValueInstance table =
-        "instance InputValue " <> modelName <> " where inputValue = IHP.ModelSupport.recordToInputValue\n"
-    where
-        modelName = qualifiedConstructorNameFromTableName table.name
-
--- | Returns all the type arguments of the data structure for an entity
-dataTypeArguments :: (?schema :: Schema) => CreateTable -> [Text]
-dataTypeArguments table = (map columnNameToFieldName belongsToVariables) <> hasManyVariables
-    where
-        belongsToVariables = variableAttributes table |> map (.name)
-        hasManyVariables =
-            columnsReferencingTable table.name
-            |> compileQueryBuilderFields
-            |> map snd
-
--- | Returns the field names and types for the @data MyRecord = MyRecord { .. }@ for a given table
-dataFields :: (?schema :: Schema) => CreateTable -> [(Text, Text)]
-dataFields table@(CreateTable { name, columns }) = columnFields <> queryBuilderFields <> [("meta", "MetaBag")]
-    where
-        columnFields = columns |> map columnField
-
-        columnField column =
-            let fieldName = columnNameToFieldName column.name
-            in
-                ( fieldName
-                , if isVariableAttribute table column
-                        then fieldName
-                        else haskellType table column
-                )
-
-        queryBuilderFields = columnsReferencingTable name |> compileQueryBuilderFields
-
-compileQueryBuilderFields :: [(Text, Text)] -> [(Text, Text)]
-compileQueryBuilderFields columns = map compileQueryBuilderField columns
-    where
-        compileQueryBuilderField (refTableName, refColumnName) =
-            let
-                -- Given a relationship like the following:
-                --
-                -- CREATE TABLE referrals (
-                --     user_id UUID NOT NULL,
-                --     referred_user_id UUID DEFAULT uuid_generate_v4() NOT NULL
-                -- );
-                --
-                -- We would have two fields on the @User@ record called @referrals@ which are
-                -- going to be used with fetchRelated (user >>= fetchRelated #referrals).
-                --
-                -- Of course having two fields in the same record does not work, so we have to
-                -- detect these duplicate query builder fields and use a more qualified name.
-                --
-                -- In the example this will lead to two fileds called @referralsUsers@ and @referralsReferredUsers@
-                -- being added to the data structure.
-                hasDuplicateQueryBuilder =
-                    columns
-                    |> map fst
-                    |> map columnNameToFieldName
-                    |> filter (columnNameToFieldName refTableName ==)
-                    |> length
-                    |> (\count -> count > 1)
-
-                stripIdSuffix :: Text -> Text
-                stripIdSuffix name = fromMaybe name (Text.stripSuffix "_id" name)
-
-                fieldName = if hasDuplicateQueryBuilder
-                    then
-                        (refTableName <> "_" <> (refColumnName |> stripIdSuffix))
-                        |> columnNameToFieldName
-                        |> pluralize
-                    else columnNameToFieldName refTableName
-            in
-                (fieldName, fieldName)
-
-
--- | Finds all the columns referencing a specific table via a foreign key constraint
---
--- __Example:__
---
--- Given the schema:
---
--- > CREATE TABLE users (id SERIAL, company_id INT);
--- > CREATE TABLE companies (id SERIAL);
---
--- you can do the following:
---
--- >>> columnsReferencingTable "companies"
--- [ ("users", "company_id") ]
-columnsReferencingTable :: (?schema :: Schema) => Text -> [(Text, Text)]
-columnsReferencingTable theTableName =
-    let
-        (Schema statements) = ?schema
-    in
-        statements
-        |> mapMaybe \case
-            AddConstraint { tableName, constraint = ForeignKeyConstraint { columnName, referenceTable, referenceColumn } } | referenceTable == theTableName -> Just (tableName, columnName)
-            _ -> Nothing
-
-variableAttributes :: (?schema :: Schema) => CreateTable -> [Column]
-variableAttributes table@(CreateTable { columns }) = filter (isVariableAttribute table) columns
-
-isVariableAttribute :: (?schema :: Schema) => CreateTable -> Column -> Bool
-isVariableAttribute = isRefCol
-
-
--- | Returns @True@ when the coluns is referencing another column via foreign key constraint
-isRefCol :: (?schema :: Schema) => CreateTable -> Column -> Bool
-isRefCol table column = isJust (findForeignKeyConstraint table column)
-
--- | Returns the foreign key constraint bound on the given column
-findForeignKeyConstraint :: (?schema :: Schema) => CreateTable -> Column -> Maybe Constraint
-findForeignKeyConstraint CreateTable { name } column =
-        case find isFkConstraint statements of
-            Just (AddConstraint { constraint }) -> Just constraint
-            Nothing -> Nothing
-    where
-        isFkConstraint (AddConstraint { tableName, constraint = ForeignKeyConstraint { columnName }}) = tableName == name && columnName == column.name
-        isFkConstraint _ = False
-
-        (Schema statements) = ?schema
-
-compileEnumDataDefinitions :: (?schema :: Schema) => Statement -> Text
-compileEnumDataDefinitions CreateEnumType { values = [] } = "" -- Ignore enums without any values
-compileEnumDataDefinitions enum@(CreateEnumType { name, values }) =
-        "data " <> modelName <> " = " <> (intercalate " | " valueConstructors) <> " deriving (Eq, Show, Read, Enum, Bounded, Ord)\n"
-        <> "instance FromField " <> modelName <> " where\n"
-        <> indent (unlines (map compileFromFieldInstanceForValue values))
-        <> "    fromField field (Just value) = returnError ConversionFailed field (\"Unexpected value for enum value. Got: \" <> Data.String.Conversions.cs value)\n"
-        <> "    fromField field Nothing = returnError UnexpectedNull field \"Unexpected null for enum value\"\n"
-        <> "instance Default " <> modelName <> " where def = " <> enumValueToConstructorName (unsafeHead values) <> "\n"
-        <> "instance ToField " <> modelName <> " where\n" <> indent (unlines (map compileToFieldInstanceForValue values))
-        <> "instance InputValue " <> modelName <> " where\n" <> indent (unlines (map compileInputValue values))
-        <> "instance DeepSeq.NFData " <> modelName <> " where" <> " rnf a = seq a ()" <> "\n"
-        <> "instance IHP.Controller.Param.ParamReader " <> modelName <> " where readParameter = IHP.Controller.Param.enumParamReader; readParameterJSON = IHP.Controller.Param.enumParamReaderJSON\n"
-    where
-        modelName = tableNameToModelName name
-        valueConstructors = map enumValueToConstructorName values
-
-        enumValueToConstructorName :: Text -> Text
-        enumValueToConstructorName enumValue = if isEnumValueUniqueInSchema enumValue
-                then enumValueToControllerName enumValue
-                else modelName <> (enumValueToControllerName enumValue)
-
-        compileFromFieldInstanceForValue value = "fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 " <> tshow value <> ") = pure " <> enumValueToConstructorName value
-        compileToFieldInstanceForValue value = "toField " <> enumValueToConstructorName value <> " = toField (" <> tshow value <> " :: Text)"
-        compileInputValue value = "inputValue " <> enumValueToConstructorName value <> " = " <> tshow value <> " :: Text"
-
-        -- Let's say we have a schema like this:
-        --
-        -- > CREATE TYPE property_type AS ENUM ('APARTMENT', 'HOUSE');
-        -- > CREATE TYPE apartment_type AS ENUM ('LOFT', 'APARTMENT');
-        --
-        -- A naive enum implementation will generate these data constructors:
-        --
-        -- > data PropertyType = Apartment | House
-        -- > data ApartmentType = Loft | Apartment
-        --
-        -- Now we have two data constructors with the name 'Apartment'. This fails to compile.
-        --
-        -- To avoid this we detect if a name is unique across the schema. When it's not unique
-        -- we use the following naming schema:
-        --
-        -- > data PropertyType = PropertyTypeApartment | House
-        -- > data ApartmentType = Loft | ApartmentTypeApartment
-        --
-        -- This function returns True if the given enumValue (like 'APARTMENT') is unique across the schema.
-        isEnumValueUniqueInSchema :: Text -> Bool
-        isEnumValueUniqueInSchema enumValue =
-                ?schema
-                |> \case Schema statements -> statements
-                |> filter (\case
-                        CreateEnumType { name, values } | enumValue `elem` values -> True
-                        _                                                         -> False
-                    )
-                |> length
-                |> \count -> count == 1
-
-compileToRowValues :: [Text] -> Text
-compileToRowValues bindingValues | length bindingValues == 1 = "Only (" <> (unsafeHead bindingValues) <> ")"
-compileToRowValues bindingValues = "(" <> intercalate ") :. (" (map (\list -> if length list == 1 then "Only (" <> (unsafeHead list) <> ")" else intercalate ", " list) (chunksOf 8 bindingValues)) <> ")"
-
--- When we do an INSERT or UPDATE query like @INSERT INTO values (uuids) VALUES (?)@ where the type of @uuids@ is @UUID[]@
--- we need to add a typecast to the placeholder @?@, otherwise this will throw an sql error
--- See https://github.com/digitallyinduced/ihp/issues/593
--- See https://github.com/digitallyinduced/ihp/issues/913
-columnPlaceholder :: Column -> Text
-columnPlaceholder column@Column { columnType } = if columnPlaceholderNeedsTypecast column
-        then "? :: " <> SqlCompiler.compilePostgresType columnType
-        else "?"
-    where
-        columnPlaceholderNeedsTypecast Column { columnType = PArray {} } = True
-        columnPlaceholderNeedsTypecast _ = False
-
-qualifiedConstructorNameFromTableName :: Text -> Text
-qualifiedConstructorNameFromTableName unqualifiedName = "Generated.ActualTypes." <> (tableNameToModelName unqualifiedName)
-
-compileCreate :: CreateTable -> Text
-compileCreate table@(CreateTable { name, columns }) =
-    let
-        writableColumns = onlyWritableColumns columns
-        modelName = qualifiedConstructorNameFromTableName name
-        columnNames = commaSep (map (.name) writableColumns)
-        values = commaSep (map columnPlaceholder writableColumns)
-
-        toBinding column@(Column { name }) =
-                if hasExplicitOrImplicitDefault column && not isArrayColumn
-                    then "fieldWithDefault #" <> columnNameToFieldName name <> " model"
-                    else "model." <> columnNameToFieldName name
-            where
-                -- We cannot use DEFAULT with array columns as postgres will throw an error:
-                --
-                -- > DEFAULT is not allowed in this context
-                --
-                -- To walk around this error, we explicitly specify an empty array.
-                isArrayColumn = case column.columnType of
-                    PArray _ -> True
-                    _        -> False
-
-
-        bindings :: [Text]
-        bindings = map toBinding writableColumns
-
-        createManyFieldValues :: Text
-        createManyFieldValues = if null bindings
-                then "()"
-                else "(List.concat $ List.map (\\model -> [" <> (intercalate ", " (map (\b -> "toField (" <> b <> ")") bindings)) <> "]) models)"
-    in
-        "instance CanCreate " <> modelName <> " where\n"
-        <> indent (
-            "create :: (?modelContext :: ModelContext) => " <> modelName <> " -> IO " <> modelName <> "\n"
-                <> "create model = do\n"
-                <> indent ("sqlQuerySingleRow \"INSERT INTO " <> name <> " (" <> columnNames <> ") VALUES (" <> values <> ") RETURNING " <> columnNames <> "\" (" <> compileToRowValues bindings <> ")\n")
-                <> "createMany [] = pure []\n"
-                <> "createMany models = do\n"
-                <> indent ("sqlQuery (Query $ \"INSERT INTO " <> name <> " (" <> columnNames <> ") VALUES \" <> (ByteString.intercalate \", \" (List.map (\\_ -> \"(" <> values <> ")\") models)) <> \" RETURNING " <> columnNames <> "\") " <> createManyFieldValues <> "\n"
-                    )
-            )
-        <> indent (
-            "createRecordDiscardResult :: (?modelContext :: ModelContext) => " <> modelName <> " -> IO ()\n"
-                <> "createRecordDiscardResult model = do\n"
-                <> indent ("sqlExecDiscardResult \"INSERT INTO " <> name <> " (" <> columnNames <> ") VALUES (" <> values <> ")\" (" <> compileToRowValues bindings <> ")\n")
-            )
-
-commaSep :: [Text] -> Text
-commaSep = intercalate ", "
-
-toBinding :: Text -> Column -> Text
-toBinding modelName Column { name } = "let " <> modelName <> "{" <> columnNameToFieldName name <> "} = model in " <> columnNameToFieldName name
-
-onlyWritableColumns columns = columns |> filter (\Column { generator } -> isNothing generator)
-
-compileUpdate :: CreateTable -> Text
-compileUpdate table@(CreateTable { name, columns }) =
-    let
-        modelName = qualifiedConstructorNameFromTableName name
-        writableColumns = onlyWritableColumns columns
-
-        toUpdateBinding Column { name } = "fieldWithUpdate #" <> columnNameToFieldName name <> " model"
-        toPrimaryKeyBinding Column { name } = "model." <> columnNameToFieldName name
-
-        bindings :: Text
-        bindings =
-            let
-                bindingValues = map toUpdateBinding writableColumns <> map toPrimaryKeyBinding (primaryKeyColumns table)
-            in
-                compileToRowValues bindingValues
-
-        updates = commaSep (map (\column -> column.name <> " = " <> columnPlaceholder column ) writableColumns)
-
-        columnNames = writableColumns
-                |> map (.name)
-                |> intercalate ", "
-
-        primaryKeyPattern = case primaryKeyColumns table of
-                                [] -> error $ "Impossible happened in compileUpdate. No primary keys found for table " <> cs name <> ". At least one primary key is required."
-                                [col] -> col.name
-                                cols -> "(" <> commaSep (map (\col -> col.name) cols) <> ")"
-
-        primaryKeyParameters = case primaryKeyColumns table of
-                                [] -> error $ "Impossible happened in compileUpdate. No primary keys found for table " <> cs name <> ". At least one primary key is required."
-                                [col] -> "?"
-                                cols -> "(" <> commaSep (map (const "?") (primaryKeyColumns table)) <> ")"
-    in
-        "instance CanUpdate " <> modelName <> " where\n"
-        <> indent ("updateRecord model = do\n"
-                <> indent (
-                    "sqlQuerySingleRow \"UPDATE " <> name <> " SET " <> updates <> " WHERE " <> primaryKeyPattern <> " = "<> primaryKeyParameters <> " RETURNING " <> columnNames <> "\" (" <> bindings <> ")\n"
-                )
-            )
-        <> indent ("updateRecordDiscardResult model = do\n"
-                <> indent (
-                    "sqlExecDiscardResult \"UPDATE " <> name <> " SET " <> updates <> " WHERE " <> primaryKeyPattern <> " = "<> primaryKeyParameters <> "\" (" <> bindings <> ")\n"
-                )
-            )
-
-compileFromRowInstance :: (?schema :: Schema) => CreateTable -> Text
-compileFromRowInstance table@(CreateTable { name, columns }) = cs [i|
-instance FromRow #{modelName} where
-    fromRow = do
-#{unsafeInit . indent . indent . unlines $ map columnBinding columnNames}
-        let theRecord = #{modelName} #{intercalate " " (map compileField (dataFields table))}
-        pure theRecord
-
-|]
-    where
-        modelName = qualifiedConstructorNameFromTableName name
-        columnNames = map (columnNameToFieldName . (.name)) columns
-        columnBinding columnName = columnName <> " <- field"
-
-        referencing = columnsReferencingTable table.name
-
-        compileField (fieldName, _)
-            | isColumn fieldName = fieldName
-            | isOneToManyField fieldName = let (Just ref) = find (\(n, _) -> columnNameToFieldName n == fieldName) referencing in compileSetQueryBuilder ref
-            | fieldName == "meta" = "def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }"
-            | otherwise = "def"
-
-        isPrimaryKey name = name `elem` primaryKeyColumnNames table.primaryKeyConstraint
-        isColumn name = name `elem` columnNames
-        isOneToManyField fieldName = fieldName `elem` (referencing |> map (columnNameToFieldName . fst))
-
-        compileSetQueryBuilder (refTableName, refFieldName) = "(QueryBuilder.filterWhere (#" <> columnNameToFieldName refFieldName <> ", " <> primaryKeyField <> ") (QueryBuilder.query @" <> tableNameToModelName refTableName <> "))"
-            where
-                -- | When the referenced column is nullable, we have to wrap the @Id@ in @Just@
-                primaryKeyField :: Text
-                primaryKeyField = if refColumn.notNull then actualPrimaryKeyField else "Just " <> actualPrimaryKeyField
-                actualPrimaryKeyField :: Text
-                actualPrimaryKeyField = case primaryKeyColumns table of
-                        [] -> error $ "Impossible happened in compilePrimaryKeyInstance. No primary keys found for table " <> cs name <> ". At least one primary key is required."
-                        [pk] -> columnNameToFieldName pk.name
-                        pks -> error $ "No support yet for composite foreign keys. Tables cannot have foreign keys to table '" <> cs name <> "' which has more than one column as its primary key."
-
-
-                (Just refTable) = let (Schema statements) = ?schema in
-                        statements
-                        |> find \case
-                                StatementCreateTable CreateTable { name } -> name == refTableName
-                                otherwise -> False
-
-                refColumn :: Column
-                refColumn = refTable
-                        |> \case StatementCreateTable CreateTable { columns } -> columns
-                        |> find (\col -> col.name == refFieldName)
-                        |> \case
-                            Just refColumn -> refColumn
-                            Nothing -> error (cs $ "Could not find " <> refTable.name <> "." <> refFieldName <> " referenced by a foreign key constraint. Make sure that there is no typo in the foreign key constraint")
-
-        compileQuery column@(Column { name }) = columnNameToFieldName name <> " = (" <> toBinding modelName column <> ")"
-        -- compileQuery column@(Column { name }) | isReferenceColum column = columnNameToFieldName name <> " = (" <> toBinding modelName column <> ")"
-        --compileQuery (HasMany hasManyName inverseOf) = columnNameToFieldName hasManyName <> " = (QueryBuilder.filterWhere (Data.Proxy.Proxy @" <> tshow relatedFieldName <> ", " <> (fromJust $ toBinding' (tableNameToModelName name) relatedIdField)  <> ") (QueryBuilder.query @" <> tableNameToModelName hasManyName <>"))"
-        --    where
-        --        compileInverseOf Nothing = (columnNameToFieldName (singularize name)) <> "Id"
-        --        compileInverseOf (Just name) = columnNameToFieldName (singularize name)
-        --        relatedFieldName = compileInverseOf inverseOf
-        --        relatedIdField = relatedField "id"
-        --        relatedForeignKeyField = relatedField relatedFieldName
-        --        relatedField :: Text -> Attribute
-        --        relatedField relatedFieldName =
-        --            let
-        --                isFieldName name (Field fieldName _) = (columnNameToFieldName fieldName) == name
-        --                (Table _ attributes) = relatedTable
-        --            in case find (isFieldName relatedFieldName) (fieldsOnly attributes) of
-        --                Just a -> a
-        --                Nothing ->
-        --                    let (Table tableName _) = relatedTable
-        --                    in error (
-        --                            "Could not find field "
-        --                            <> show relatedFieldName
-        --                            <> " in table"
-        --                            <> cs tableName
-        --                            <> " "
-        --                            <> (show $ fieldsOnly attributes)
-        --                            <> ".\n\nThis is caused by `+ hasMany " <> show hasManyName <> "`"
-        --                        )
-        --        relatedTable = case find (\(Table tableName _) -> tableName == hasManyName) database of
-        --            Just t -> t
-        --            Nothing -> error ("Could not find table " <> show hasManyName)
-        --        toBinding' modelName attributes =
-        --            case relatedForeignKeyField of
-        --                Field _ fieldType | allowNull fieldType -> Just $ "Just (" <> fromJust (toBinding modelName attributes) <> ")"
-        --                otherwise -> toBinding modelName attributes
-
-compileBuild :: (?schema :: Schema) => CreateTable -> Text
-compileBuild table@(CreateTable { name, columns }) =
-    let
-        constructor = qualifiedConstructorNameFromTableName name
-    in
-        "instance Record " <> constructor <> " where\n"
-        <> "    {-# INLINE newRecord #-}\n"
-        <> "    newRecord = " <> constructor <> " " <> unwords (map toDefaultValueExpr columns) <> " " <> (columnsReferencingTable name |> map (const "def") |> unwords) <> " def\n"
-
-
-compileDefaultIdInstance :: CreateTable -> Text
-compileDefaultIdInstance table = "instance Default (Id' \"" <> table.name <> "\") where def = Id def"
-
-
-toDefaultValueExpr :: Column -> Text
-toDefaultValueExpr Column { columnType, notNull, defaultValue = Just theDefaultValue } =
-            let
-                wrapNull False value = "(Just " <> value <> ")"
-                wrapNull True value = value
-
-                isNullExpr (VarExpression varName) = toUpper varName == "NULL"
-                isNullExpr _ = False
-
-                -- We remove type casts here, as we need the actual value literal for setting our default value
-                theNormalizedDefaultValue = theDefaultValue |> SchemaDesigner.removeTypeCasts
-            in
-                if isNullExpr theDefaultValue
-                    then "Nothing"
-                    else
-                        case columnType of
-                            PText -> case theNormalizedDefaultValue of
-                                TextExpression value -> wrapNull notNull (tshow value)
-                                otherwise            -> error ("toDefaultValueExpr: TEXT column needs to have a TextExpression as default value. Got: " <> show otherwise)
-                            PBoolean -> case theNormalizedDefaultValue of
-                                VarExpression value -> wrapNull notNull (tshow (toLower value == "true"))
-                                otherwise           -> error ("toDefaultValueExpr: BOOL column needs to have a VarExpression as default value. Got: " <> show otherwise)
-                            PDouble -> case theNormalizedDefaultValue of
-                                DoubleExpression value -> wrapNull notNull (tshow value)
-                                IntExpression value -> wrapNull notNull (tshow value)
-                                otherwise           -> error ("toDefaultValueExpr: DOUBLE column needs to have a DoubleExpression as default value. Got: " <> show otherwise)
-                            _ -> "def"
-toDefaultValueExpr _ = "def"
-
-compileHasTableNameInstance :: (?schema :: Schema) => CreateTable -> Text
-compileHasTableNameInstance table@(CreateTable { name }) =
-    "type instance GetTableName (" <> tableNameToModelName name <> "' " <> unwords (map (const "_") (dataTypeArguments table)) <>  ") = " <> tshow name <> "\n"
-    <> "type instance GetModelByTableName " <> tshow name <> " = Generated.ActualTypes." <> tableNameToModelName name <> "\n"
-
-compilePrimaryKeyInstance :: (?schema :: Schema) => CreateTable -> Text
-compilePrimaryKeyInstance table@(CreateTable { name, columns, constraints }) = [trimming|type instance PrimaryKey $symbol = $idType|] <> "\n"
-    where
-        symbol = tshow name
-        idType :: Text
-        idType = case primaryKeyColumns table of
-                [] -> error $ "Impossible happened in compilePrimaryKeyInstance. No primary keys found for table " <> cs name <> ". At least one primary key is required."
-                [column] -> atomicType column.columnType -- PrimaryKey User = UUID
-                cs -> "(" <> intercalate ", " (map colType cs) <> ")" -- PrimaryKey PostsTag = (Id' "posts", Id' "tags")
-            where
-                colType column = haskellType table column
-
-compileFilterPrimaryKeyInstance :: (?schema :: Schema) => CreateTable -> Text
-compileFilterPrimaryKeyInstance table@(CreateTable { name, columns, constraints }) = cs [i|
-instance QueryBuilder.FilterPrimaryKey "#{name}" where
-    filterWhereId #{primaryKeyPattern} builder =
-        builder |> #{intercalate " |> " primaryKeyFilters}
-    {-# INLINE filterWhereId #-}
-|]
-    where
-        primaryKeyPattern = case primaryKeyColumns table of
-            [] -> error $ "Impossible happened in compilePrimaryKeyInstance. No primary keys found for table " <> cs name <> ". At least one primary key is required."
-            [c] -> columnNameToFieldName c.name
-            cs -> "(Id (" <> intercalate ", " (map (columnNameToFieldName . (.name)) cs) <> "))"
-
-        primaryKeyFilters :: [Text]
-        primaryKeyFilters = map primaryKeyFilter $ primaryKeyColumns table
-
-        primaryKeyFilter :: Column -> Text
-        primaryKeyFilter Column {name} = "QueryBuilder.filterWhere (#" <> columnNameToFieldName name <> ", " <> columnNameToFieldName name <> ")"
-
-compileTableInstance :: (?schema :: Schema) => CreateTable -> Text
-compileTableInstance table@(CreateTable { name, columns, constraints }) = cs [i|
-instance #{instanceHead} where
-    tableName = \"#{name}\"
-    tableNameByteString = Data.Text.Encoding.encodeUtf8 \"#{name}\"
-    columnNames = #{columnNames}
-    primaryKeyColumnNames = #{primaryKeyColumnNames}
-    primaryKeyConditionForId (#{pattern}) = #{condition}
-    {-# INLINABLE primaryKeyConditionForId #-}
-|]
-    where
-        instanceHead :: Text
-        instanceHead = instanceConstraints <> " => IHP.ModelSupport.Table (" <> compileTypePattern table <> ")"
-            where
-                instanceConstraints =
-                    table
-                    |> primaryKeyColumns
-                    |> map (.name)
-                    |> map columnNameToFieldName
-                    |> filter (\field -> field `elem` (dataTypeArguments table))
-                    |> map (\field -> "ToField " <> field)
-                    |> intercalate ", "
-                    |> \inner -> "(" <> inner <> ")"
-
-        primaryKeyColumnNames :: [Text]
-        primaryKeyColumnNames = primaryKeyColumns table |> map (.name)
-
-        primaryKeyFieldNames :: [Text]
-        primaryKeyFieldNames = primaryKeyColumnNames |> map columnNameToFieldName
-
-        pattern :: Text
-        pattern = "Id (" <> intercalate ", " primaryKeyFieldNames <> ")"
-
-        condition :: Text
-        condition = case primaryKeyColumns table of
-                            [] -> error $ "Impossible happened in compileUpdate. No primary keys found for table " <> cs name <> ". At least one primary key is required."
-                            [column] -> primaryKeyToCondition column
-                            cols -> "Many [Plain \"(\", " <> intercalate ", Plain \",\", " (map primaryKeyToCondition cols)<> ", Plain \")\"]"
-
-        primaryKeyToCondition :: Column -> Text
-        primaryKeyToCondition column = "toField " <> columnNameToFieldName column.name
-
-        columnNames = columns
-                |> map (.name)
-                |> tshow
-
-compileGetModelName :: (?schema :: Schema) => CreateTable -> Text
-compileGetModelName table@(CreateTable { name }) = "type instance GetModelName (" <> tableNameToModelName name <> "' " <> unwords (map (const "_") (dataTypeArguments table)) <>  ") = " <> tshow (tableNameToModelName name) <> "\n"
-
-compileDataTypePattern :: (?schema :: Schema) => CreateTable -> Text
-compileDataTypePattern table@(CreateTable { name }) = tableNameToModelName name <> " " <> unwords (table |> dataFields |> map fst)
-
-compileTypePattern :: (?schema :: Schema) => CreateTable -> Text
-compileTypePattern table@(CreateTable { name }) = tableNameToModelName name <> "' " <> unwords (dataTypeArguments table)
-
-compileInclude :: (?schema :: Schema) => CreateTable -> Text
-compileInclude table@(CreateTable { name, columns }) = (belongsToIncludes <> hasManyIncludes) |> unlines
-    where
-        belongsToIncludes = map compileBelongsTo (filter (isRefCol table) columns)
-        hasManyIncludes = columnsReferencingTable name
-                |> (\refs -> zip (map fst refs) (map fst (compileQueryBuilderFields refs)))
-                |> map compileHasMany
-        typeArgs = dataTypeArguments table
-        modelName = tableNameToModelName name
-        modelConstructor = modelName <> "'"
-
-        includeType :: Text -> Text -> Text
-        includeType fieldName includedType = "type instance Include " <> tshow fieldName <> " (" <> leftModelType <> ") = " <> rightModelType
-            where
-                leftModelType = unwords (modelConstructor:typeArgs)
-                rightModelType = unwords (modelConstructor:(map compileTypeVariable' typeArgs))
-                compileTypeVariable' name | name == fieldName = includedType
-                compileTypeVariable' name = name
-
-        compileBelongsTo :: Column -> Text
-        compileBelongsTo column = includeType (columnNameToFieldName column.name) ("(GetModelById " <> columnNameToFieldName column.name <> ")")
-
-        compileHasMany :: (Text, Text) -> Text
-        compileHasMany (refTableName, refColumnName) = includeType refColumnName ("[" <> tableNameToModelName refTableName <> "]")
-
-
-compileSetFieldInstances :: (?schema :: Schema) => CreateTable -> Text
-compileSetFieldInstances table@(CreateTable { name, columns }) = unlines (map compileSetField (dataFields table))
-    where
-        setMetaField = "instance SetField \"meta\" (" <> compileTypePattern table <>  ") MetaBag where\n    {-# INLINE setField #-}\n    setField newValue (" <> compileDataTypePattern table <> ") = " <> tableNameToModelName name <> " " <> (unwords (map (.name) columns)) <> " newValue"
-        modelName = tableNameToModelName name
-        typeArgs = dataTypeArguments table
-        compileSetField (name, fieldType) =
-            "instance SetField " <> tshow name <> " (" <> compileTypePattern table <>  ") " <> fieldType <> " where\n" <>
-            "    {-# INLINE setField #-}\n" <>
-            "    setField newValue (" <> compileDataTypePattern table <> ") =\n" <>
-            "        " <> modelName <> " " <> (unwords (map compileAttribute (table |> dataFields |> map fst)))
-            where
-                compileAttribute name'
-                    | name' == name = "newValue"
-                    | name' == "meta" = "(meta { touchedFields = \"" <> name <> "\" : touchedFields meta })"
-                    | otherwise = name'
-
-compileUpdateFieldInstances :: (?schema :: Schema) => CreateTable -> Text
-compileUpdateFieldInstances table@(CreateTable { name, columns }) = unlines (map compileSetField (dataFields table))
-    where
-        modelName = tableNameToModelName name
-        typeArgs = dataTypeArguments table
-        compileSetField (name, fieldType) = "instance UpdateField " <> tshow name <> " (" <> compileTypePattern table <>  ") (" <> compileTypePattern' name  <> ") " <> valueTypeA <> " " <> valueTypeB <> " where\n    {-# INLINE updateField #-}\n    updateField newValue (" <> compileDataTypePattern table <> ") = " <> modelName <> " " <> (unwords (map compileAttribute (table |> dataFields |> map fst)))
-            where
-                (valueTypeA, valueTypeB) =
-                    if name `elem` typeArgs
-                        then (name, name <> "'")
-                        else (fieldType, fieldType)
-
-                compileAttribute name'
-                    | name' == name = "newValue"
-                    | name' == "meta" = "(meta { touchedFields = \"" <> name <> "\" : touchedFields meta })"
-                    | otherwise = name'
-
-                compileTypePattern' ::  Text -> Text
-                compileTypePattern' name = tableNameToModelName table.name <> "' " <> unwords (map (\f -> if f == name then name <> "'" else f) (dataTypeArguments table))
-
-compileHasFieldId :: (?schema :: Schema) => CreateTable -> Text
-compileHasFieldId table@CreateTable { name, primaryKeyConstraint } = cs [i|
-instance HasField "id" #{tableNameToModelName name} (Id' "#{name}") where
-    getField (#{compileDataTypePattern table}) = #{compilePrimaryKeyValue}
-    {-# INLINE getField #-}
-|]
-    where
-        compilePrimaryKeyValue = case primaryKeyColumnNames primaryKeyConstraint of
-            [id] -> columnNameToFieldName id
-            ids -> "Id (" <> commaSep (map columnNameToFieldName ids) <> ")"
-
-needsHasFieldId :: CreateTable -> Bool
-needsHasFieldId CreateTable { primaryKeyConstraint } =
-  case primaryKeyColumnNames primaryKeyConstraint of
-    [] -> False
-    ["id"] -> False
-    _ -> True
-
-primaryKeyColumns :: CreateTable -> [Column]
-primaryKeyColumns CreateTable { name, columns, primaryKeyConstraint } =
-    map getColumn (primaryKeyColumnNames primaryKeyConstraint)
-  where
-    getColumn columnName = case find ((==) columnName . (.name)) columns of
-      Just c -> c
-      Nothing -> error ("Missing column " <> cs columnName <> " used in primary key for " <> cs name)
-
--- | Indents a block of code with 4 spaces.
---
--- Empty lines are not indented.
-indent :: Text -> Text
-indent code = code
-        |> Text.lines
-        |> map indentLine
-        |> Text.unlines
-    where
-        indentLine ""   = ""
-        indentLine line = "    " <> line
-
--- | Returns 'True' when the column has an explicit default value or when it's a SERIAL or BIGSERIAL
-hasExplicitOrImplicitDefault :: Column -> Bool
-hasExplicitOrImplicitDefault column = case column of
-        Column { defaultValue = Just _ } -> True
-        Column { columnType = PSerial } -> True
-        Column { columnType = PBigserial } -> True
-        _ -> False
diff --git a/IHP/Telemetry.hs b/IHP/Telemetry.hs
--- a/IHP/Telemetry.hs
+++ b/IHP/Telemetry.hs
@@ -12,8 +12,9 @@
 import qualified Network.Wreq as Wreq
 import qualified Control.Exception.Safe as Exception
 import qualified Crypto.Hash.SHA512 as SHA512
-import qualified System.Directory as Directory
+import qualified System.Directory.OsPath as Directory
 import qualified Data.ByteString.Base16 as Base16
+import System.OsPath (decodeUtf)
 import qualified Data.Text as T
 import qualified Data.Text.IO as TIO
 
@@ -63,7 +64,8 @@
 -- is able to get back the original path from the hash.
 getProjectId :: IO Text
 getProjectId = do
-    cwd <- Directory.getCurrentDirectory
+    cwdOsPath <- Directory.getCurrentDirectory
+    cwd <- decodeUtf cwdOsPath
     cwd
         |> cs
         |> SHA512.hash
diff --git a/IHP/Test/Database.hs b/IHP/Test/Database.hs
deleted file mode 100644
--- a/IHP/Test/Database.hs
+++ /dev/null
@@ -1,96 +0,0 @@
-module IHP.Test.Database (withIHPApp) where
-
-import IHP.Prelude
-import qualified Database.PostgreSQL.Simple as PG
-import qualified Database.PostgreSQL.Simple.Types as PG
-import qualified Data.UUID.V4 as UUID
-import qualified Data.UUID as UUID
-import qualified Data.Text as Text
-import qualified Control.Exception as Exception
-import IHP.IDE.CodeGen.MigrationGenerator (findIHPSchemaSql)
-
-
-import qualified Data.Vault.Lazy                           as Vault
-import           Network.Wai
-import           Network.Wai.Internal                      (ResponseReceived (..))
-
-
-import           IHP.ApplicationContext                    (ApplicationContext (..))
-import qualified IHP.AutoRefresh.Types                     as AutoRefresh
-import           IHP.Controller.RequestContext             (RequestBody (..), RequestContext (..))
-import           IHP.ControllerSupport                     (InitControllerContext, Controller, runActionWithNewContext)
-import           IHP.FrameworkConfig                       (ConfigBuilder (..), FrameworkConfig (..))
-import qualified IHP.FrameworkConfig                       as FrameworkConfig
-import           IHP.ModelSupport                          (createModelContext, Id')
-import           IHP.Prelude
-import           IHP.Log.Types
-import qualified IHP.PGListener as PGListener
-import IHP.Controller.Session (sessionVaultKey)
-
-import qualified System.Process as Process
-import IHP.Test.Mocking
-
-withConnection databaseUrl = Exception.bracket (PG.connectPostgreSQL databaseUrl) PG.close
-
--- | Create contexts that can be used for mocking
-withIHPApp :: (InitControllerContext application) => application -> ConfigBuilder -> (MockContext application -> IO ()) -> IO ()
-withIHPApp application configBuilder hspecAction = do
-    FrameworkConfig.withFrameworkConfig configBuilder \frameworkConfig -> do
-        let FrameworkConfig { dbPoolMaxConnections, dbPoolIdleTime } = frameworkConfig
-
-        logger <- newLogger def { level = Warn } -- don't log queries
-
-
-        withTestDatabase frameworkConfig.databaseUrl \testDatabaseUrl -> do
-            modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections testDatabaseUrl logger
-
-            PGListener.withPGListener modelContext \pgListener -> do
-                autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener)
-                let sessionVault = Vault.insert sessionVaultKey mempty Vault.empty
-                let applicationContext = ApplicationContext { modelContext = modelContext, autoRefreshServer, frameworkConfig, pgListener }
-
-                let requestContext = RequestContext
-                     { request = defaultRequest {vault = sessionVault}
-                     , requestBody = FormBody [] []
-                     , respond = const (pure ResponseReceived)
-                     , frameworkConfig = frameworkConfig }
-
-                (hspecAction MockContext { .. })
-
-withTestDatabase masterDatabaseUrl callback = do
-    testDatabaseName <- randomDatabaseName
-
-    withConnection masterDatabaseUrl \masterConnection ->
-        Exception.bracket_
-            (PG.execute masterConnection "CREATE DATABASE ?" [PG.Identifier testDatabaseName])
-            (
-                -- The WITH FORCE is required to force close open connections
-                -- Otherwise the DROP DATABASE takes a few seconds to execute
-                PG.execute masterConnection "DROP DATABASE ? WITH (FORCE)" [PG.Identifier testDatabaseName]
-            )
-            (do
-                importSchema (testDatabaseUrl masterDatabaseUrl testDatabaseName)
-                callback (testDatabaseUrl masterDatabaseUrl testDatabaseName)
-            )
-
-testDatabaseUrl :: ByteString -> Text -> ByteString
-testDatabaseUrl masterDatabaseUrl testDatabaseName =
-    masterDatabaseUrl
-        |> cs
-        |> Text.replace "postgresql:///app" ("postgresql:///" <> testDatabaseName)
-        |> cs
-
-randomDatabaseName :: IO Text
-randomDatabaseName = do
-    databaseId <- UUID.nextRandom
-    pure ("test-" <> UUID.toText databaseId)
-
-importSchema :: ByteString -> IO ()
-importSchema databaseUrl = do
-    -- We use the system psql to handle the initial Schema Import as it can handle
-    -- complex Schema including variations in formatting, custom types, functions, and table definitions.
-    let importSql file = Process.callCommand ("psql " <> (cs databaseUrl) <> " < " <> file)
-
-    ihpSchemaSql <- findIHPSchemaSql
-    importSql ihpSchemaSql
-    importSql "Application/Schema.sql"
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 digitally induced GmbH
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Test/IDE/CodeGeneration/ControllerGenerator.hs b/Test/IDE/CodeGeneration/ControllerGenerator.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/CodeGeneration/ControllerGenerator.hs
@@ -0,0 +1,300 @@
+{-|
+Module: Test.IDE.CodeGeneration.ControllerGenerator
+Copyright: (c) digitally induced GmbH, 2020
+-}
+module Test.IDE.CodeGeneration.ControllerGenerator where
+
+import Test.Hspec
+import IHP.Prelude
+import qualified IHP.IDE.CodeGen.ControllerGenerator as ControllerGenerator
+import IHP.IDE.CodeGen.Types
+import IHP.Postgres.Types
+
+
+tests = do
+    describe "Controller Generator Tests:" do
+        let schema = [
+                    StatementCreateTable (table "pages") {
+                        columns = [
+                            (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                        ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    },
+                    StatementCreateTable (table "people") {
+                        columns = [
+                            (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                            , (col "name" PText) { notNull = True }
+                            , (col "email" PText) { notNull = True }
+                        ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    },
+                    StatementCreateTable (table "page_comments") {
+                        columns = [
+                            (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                        ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    }
+                ]
+
+        let makeConfig controllerName modelName applicationName paginationEnabled =
+                ControllerGenerator.defaultControllerConfig
+                    { ControllerGenerator.controllerName = controllerName
+                    , ControllerGenerator.modelName = modelName
+                    , ControllerGenerator.applicationName = applicationName
+                    , ControllerGenerator.paginationEnabled = paginationEnabled
+                    }
+
+        it "should build a controller with name \"pages\"" do
+            let rawControllerName = "pages"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let pagination = False
+            let builtPlan = ControllerGenerator.buildPlan' schema (makeConfig controllerName modelName applicationName pagination)
+
+            builtPlan `shouldBe`
+                [ CreateFile {filePath = "Web/Controller/Pages.hs", fileContent = "module Web.Controller.Pages where\n\nimport Web.Controller.Prelude\nimport Web.View.Pages.Index\nimport Web.View.Pages.New\nimport Web.View.Pages.Edit\nimport Web.View.Pages.Show\n\ninstance Controller PagesController where\n    action PagesAction = do\n        pages <- query @Page |> fetch\n        render IndexView { .. }\n\n    action NewPageAction = do\n        let page = newRecord\n        render NewView { .. }\n\n    action ShowPageAction { pageId } = do\n        page <- fetch pageId\n        render ShowView { .. }\n\n    action EditPageAction { pageId } = do\n        page <- fetch pageId\n        render EditView { .. }\n\n    action UpdatePageAction { pageId } = do\n        page <- fetch pageId\n        page\n            |> buildPage\n            |> ifValid \\case\n                Left page -> render EditView { .. }\n                Right page -> do\n                    page <- page |> updateRecord\n                    setSuccessMessage \"Page updated\"\n                    redirectTo EditPageAction { .. }\n\n    action CreatePageAction = do\n        let page = newRecord @Page\n        page\n            |> buildPage\n            |> ifValid \\case\n                Left page -> render NewView { .. } \n                Right page -> do\n                    page <- page |> createRecord\n                    setSuccessMessage \"Page created\"\n                    redirectTo PagesAction\n\n    action DeletePageAction { pageId } = do\n        page <- fetch pageId\n        deleteRecord page\n        setSuccessMessage \"Page deleted\"\n        redirectTo PagesAction\n\nbuildPage page = page\n    |> fill @'[]\n"}
+                , AppendToFile {filePath = "Web/Routes.hs", fileContent = "\ninstance AutoRoute PagesController\n\n"}
+                , AppendToFile {filePath = "Web/Types.hs", fileContent = "\ndata PagesController\n    = PagesAction\n    | NewPageAction\n    | ShowPageAction { pageId :: !(Id Page) }\n    | CreatePageAction\n    | EditPageAction { pageId :: !(Id Page) }\n    | UpdatePageAction { pageId :: !(Id Page) }\n    | DeletePageAction { pageId :: !(Id Page) }\n    deriving (Eq, Show, Data)\n"}
+                , AppendToMarker {marker = "-- Controller Imports", filePath = "Web/FrontController.hs", fileContent = "import Web.Controller.Pages"}
+                , AppendToMarker {marker = "-- Generator Marker", filePath = "Web/FrontController.hs", fileContent = "        , parseRoute @PagesController"}
+                , EnsureDirectory {directory = "Web/View/Pages"}
+                , CreateFile {filePath = "Web/View/Pages/Index.hs", fileContent = "module Web.View.Pages.Index where\nimport Web.View.Prelude\n\ndata IndexView = IndexView { pages :: [Page] }\n\ninstance View IndexView where\n    html IndexView { .. } = [hsx|\n        {breadcrumb}\n\n        <h1>Index<a href={pathTo NewPageAction} class=\"btn btn-primary ms-4\">+ New</a></h1>\n        <div class=\"table-responsive\">\n            <table class=\"table\">\n                <thead>\n                    <tr>\n                        <th>Page</th>\n                        <th></th>\n                        <th></th>\n                        <th></th>\n                    </tr>\n                </thead>\n                <tbody>{forEach pages renderPage}</tbody>\n            </table>\n            \n        </div>\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                ]\n\nrenderPage :: Page -> Html\nrenderPage page = [hsx|\n    <tr>\n        <td>{page}</td>\n        <td><a href={ShowPageAction page.id}>Show</a></td>\n        <td><a href={EditPageAction page.id} class=\"text-muted\">Edit</a></td>\n        <td><a href={DeletePageAction page.id} class=\"js-delete text-muted\">Delete</a></td>\n    </tr>\n|]"}
+                , AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.Index"}
+                , EnsureDirectory {directory = "Web/View/Pages"}
+                , CreateFile {filePath = "Web/View/Pages/New.hs", fileContent = "module Web.View.Pages.New where\nimport Web.View.Prelude\n\ndata NewView = NewView { page :: Page }\n\ninstance View NewView where\n    html NewView { .. } = [hsx|\n        {breadcrumb}\n        <h1>New Page</h1>\n        {renderForm page}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                , breadcrumbText \"New Page\"\n                ]\n\nrenderForm :: Page -> Html\nrenderForm page = formFor page [hsx|\n    \n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.New"}
+                , EnsureDirectory {directory = "Web/View/Pages"}
+                , CreateFile {filePath = "Web/View/Pages/Show.hs", fileContent = "module Web.View.Pages.Show where\nimport Web.View.Prelude\n\ndata ShowView = ShowView { page :: Page }\n\ninstance View ShowView where\n    html ShowView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Show Page</h1>\n        <p>{page}</p>\n\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                            [ breadcrumbLink \"Pages\" PagesAction\n                            , breadcrumbText \"Show Page\"\n                            ]"}
+                , AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.Show"}
+                , EnsureDirectory {directory = "Web/View/Pages"}
+                , CreateFile {filePath = "Web/View/Pages/Edit.hs", fileContent = "module Web.View.Pages.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { page :: Page }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit Page</h1>\n        {renderForm page}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                , breadcrumbText \"Edit Page\"\n                ]\n\nrenderForm :: Page -> Html\nrenderForm page = formFor page [hsx|\n    \n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.Edit"}
+                ]
+
+        it "should build a controller with name \"page\"" do
+            let rawControllerName = "page"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let pagination = False
+            let builtPlan = ControllerGenerator.buildPlan' schema (makeConfig controllerName modelName applicationName pagination)
+
+            builtPlan `shouldBe`
+                [ CreateFile {filePath = "Web/Controller/Page.hs", fileContent = "module Web.Controller.Page where\n\nimport Web.Controller.Prelude\nimport Web.View.Page.Index\nimport Web.View.Page.New\nimport Web.View.Page.Edit\nimport Web.View.Page.Show\n\ninstance Controller PageController where\n    action PagesAction = do\n        page <- query @Page |> fetch\n        render IndexView { .. }\n\n    action NewPageAction = do\n        let page = newRecord\n        render NewView { .. }\n\n    action ShowPageAction { pageId } = do\n        page <- fetch pageId\n        render ShowView { .. }\n\n    action EditPageAction { pageId } = do\n        page <- fetch pageId\n        render EditView { .. }\n\n    action UpdatePageAction { pageId } = do\n        page <- fetch pageId\n        page\n            |> buildPage\n            |> ifValid \\case\n                Left page -> render EditView { .. }\n                Right page -> do\n                    page <- page |> updateRecord\n                    setSuccessMessage \"Page updated\"\n                    redirectTo EditPageAction { .. }\n\n    action CreatePageAction = do\n        let page = newRecord @Page\n        page\n            |> buildPage\n            |> ifValid \\case\n                Left page -> render NewView { .. } \n                Right page -> do\n                    page <- page |> createRecord\n                    setSuccessMessage \"Page created\"\n                    redirectTo PagesAction\n\n    action DeletePageAction { pageId } = do\n        page <- fetch pageId\n        deleteRecord page\n        setSuccessMessage \"Page deleted\"\n        redirectTo PagesAction\n\nbuildPage page = page\n    |> fill @'[]\n"}
+                , AppendToFile {filePath = "Web/Routes.hs", fileContent = "\ninstance AutoRoute PageController\n\n"}
+                , AppendToFile {filePath = "Web/Types.hs", fileContent = "\ndata PageController\n    = PagesAction\n    | NewPageAction\n    | ShowPageAction { pageId :: !(Id Page) }\n    | CreatePageAction\n    | EditPageAction { pageId :: !(Id Page) }\n    | UpdatePageAction { pageId :: !(Id Page) }\n    | DeletePageAction { pageId :: !(Id Page) }\n    deriving (Eq, Show, Data)\n"}
+                , AppendToMarker {marker = "-- Controller Imports", filePath = "Web/FrontController.hs", fileContent = "import Web.Controller.Page"}
+                , AppendToMarker {marker = "-- Generator Marker", filePath = "Web/FrontController.hs", fileContent = "        , parseRoute @PageController"}
+                , EnsureDirectory {directory = "Web/View/Page"}
+                , CreateFile {filePath = "Web/View/Page/Index.hs", fileContent = "module Web.View.Page.Index where\nimport Web.View.Prelude\n\ndata IndexView = IndexView { page :: [Page] }\n\ninstance View IndexView where\n    html IndexView { .. } = [hsx|\n        {breadcrumb}\n\n        <h1>Index<a href={pathTo NewPageAction} class=\"btn btn-primary ms-4\">+ New</a></h1>\n        <div class=\"table-responsive\">\n            <table class=\"table\">\n                <thead>\n                    <tr>\n                        <th>Page</th>\n                        <th></th>\n                        <th></th>\n                        <th></th>\n                    </tr>\n                </thead>\n                <tbody>{forEach page renderPage}</tbody>\n            </table>\n            \n        </div>\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                ]\n\nrenderPage :: Page -> Html\nrenderPage page = [hsx|\n    <tr>\n        <td>{page}</td>\n        <td><a href={ShowPageAction page.id}>Show</a></td>\n        <td><a href={EditPageAction page.id} class=\"text-muted\">Edit</a></td>\n        <td><a href={DeletePageAction page.id} class=\"js-delete text-muted\">Delete</a></td>\n    </tr>\n|]"}
+                , AddImport {filePath = "Web/Controller/Page.hs", fileContent = "import Web.View.Page.Index"}
+                , EnsureDirectory {directory = "Web/View/Page"}
+                , CreateFile {filePath = "Web/View/Page/New.hs", fileContent = "module Web.View.Page.New where\nimport Web.View.Prelude\n\ndata NewView = NewView { page :: Page }\n\ninstance View NewView where\n    html NewView { .. } = [hsx|\n        {breadcrumb}\n        <h1>New Page</h1>\n        {renderForm page}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                , breadcrumbText \"New Page\"\n                ]\n\nrenderForm :: Page -> Html\nrenderForm page = formFor page [hsx|\n    \n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/Page.hs", fileContent = "import Web.View.Page.New"}
+                , EnsureDirectory {directory = "Web/View/Page"}
+                , CreateFile {filePath = "Web/View/Page/Show.hs", fileContent = "module Web.View.Page.Show where\nimport Web.View.Prelude\n\ndata ShowView = ShowView { page :: Page }\n\ninstance View ShowView where\n    html ShowView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Show Page</h1>\n        <p>{page}</p>\n\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                            [ breadcrumbLink \"Pages\" PagesAction\n                            , breadcrumbText \"Show Page\"\n                            ]"}
+                , AddImport {filePath = "Web/Controller/Page.hs", fileContent = "import Web.View.Page.Show"}
+                , EnsureDirectory {directory = "Web/View/Page"}
+                , CreateFile {filePath = "Web/View/Page/Edit.hs", fileContent = "module Web.View.Page.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { page :: Page }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit Page</h1>\n        {renderForm page}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                , breadcrumbText \"Edit Page\"\n                ]\n\nrenderForm :: Page -> Html\nrenderForm page = formFor page [hsx|\n    \n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/Page.hs", fileContent = "import Web.View.Page.Edit"}
+                ]
+
+        it "should build a controller with name \"page_comment\"" do
+            let rawControllerName = "page_comment"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let pagination = False
+            let builtPlan = ControllerGenerator.buildPlan' schema (makeConfig controllerName modelName applicationName pagination)
+
+            builtPlan `shouldBe`
+                [ CreateFile {filePath = "Web/Controller/PageComment.hs", fileContent = "module Web.Controller.PageComment where\n\nimport Web.Controller.Prelude\nimport Web.View.PageComment.Index\nimport Web.View.PageComment.New\nimport Web.View.PageComment.Edit\nimport Web.View.PageComment.Show\n\ninstance Controller PageCommentController where\n    action PageCommentsAction = do\n        pageComment <- query @PageComment |> fetch\n        render IndexView { .. }\n\n    action NewPageCommentAction = do\n        let pageComment = newRecord\n        render NewView { .. }\n\n    action ShowPageCommentAction { pageCommentId } = do\n        pageComment <- fetch pageCommentId\n        render ShowView { .. }\n\n    action EditPageCommentAction { pageCommentId } = do\n        pageComment <- fetch pageCommentId\n        render EditView { .. }\n\n    action UpdatePageCommentAction { pageCommentId } = do\n        pageComment <- fetch pageCommentId\n        pageComment\n            |> buildPageComment\n            |> ifValid \\case\n                Left pageComment -> render EditView { .. }\n                Right pageComment -> do\n                    pageComment <- pageComment |> updateRecord\n                    setSuccessMessage \"PageComment updated\"\n                    redirectTo EditPageCommentAction { .. }\n\n    action CreatePageCommentAction = do\n        let pageComment = newRecord @PageComment\n        pageComment\n            |> buildPageComment\n            |> ifValid \\case\n                Left pageComment -> render NewView { .. } \n                Right pageComment -> do\n                    pageComment <- pageComment |> createRecord\n                    setSuccessMessage \"PageComment created\"\n                    redirectTo PageCommentsAction\n\n    action DeletePageCommentAction { pageCommentId } = do\n        pageComment <- fetch pageCommentId\n        deleteRecord pageComment\n        setSuccessMessage \"PageComment deleted\"\n        redirectTo PageCommentsAction\n\nbuildPageComment pageComment = pageComment\n    |> fill @'[]\n"}
+                , AppendToFile {filePath = "Web/Routes.hs", fileContent = "\ninstance AutoRoute PageCommentController\n\n"}
+                , AppendToFile {filePath = "Web/Types.hs", fileContent = "\ndata PageCommentController\n    = PageCommentsAction\n    | NewPageCommentAction\n    | ShowPageCommentAction { pageCommentId :: !(Id PageComment) }\n    | CreatePageCommentAction\n    | EditPageCommentAction { pageCommentId :: !(Id PageComment) }\n    | UpdatePageCommentAction { pageCommentId :: !(Id PageComment) }\n    | DeletePageCommentAction { pageCommentId :: !(Id PageComment) }\n    deriving (Eq, Show, Data)\n"}
+                , AppendToMarker {marker = "-- Controller Imports", filePath = "Web/FrontController.hs", fileContent = "import Web.Controller.PageComment"}
+                , AppendToMarker {marker = "-- Generator Marker", filePath = "Web/FrontController.hs", fileContent = "        , parseRoute @PageCommentController"}
+                , EnsureDirectory {directory = "Web/View/PageComment"}
+                , CreateFile {filePath = "Web/View/PageComment/Index.hs", fileContent = "module Web.View.PageComment.Index where\nimport Web.View.Prelude\n\ndata IndexView = IndexView { pageComment :: [PageComment] }\n\ninstance View IndexView where\n    html IndexView { .. } = [hsx|\n        {breadcrumb}\n\n        <h1>Index<a href={pathTo NewPageCommentAction} class=\"btn btn-primary ms-4\">+ New</a></h1>\n        <div class=\"table-responsive\">\n            <table class=\"table\">\n                <thead>\n                    <tr>\n                        <th>PageComment</th>\n                        <th></th>\n                        <th></th>\n                        <th></th>\n                    </tr>\n                </thead>\n                <tbody>{forEach pageComment renderPageComment}</tbody>\n            </table>\n            \n        </div>\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"PageComments\" PageCommentsAction\n                ]\n\nrenderPageComment :: PageComment -> Html\nrenderPageComment pageComment = [hsx|\n    <tr>\n        <td>{pageComment}</td>\n        <td><a href={ShowPageCommentAction pageComment.id}>Show</a></td>\n        <td><a href={EditPageCommentAction pageComment.id} class=\"text-muted\">Edit</a></td>\n        <td><a href={DeletePageCommentAction pageComment.id} class=\"js-delete text-muted\">Delete</a></td>\n    </tr>\n|]"}
+                , AddImport {filePath = "Web/Controller/PageComment.hs", fileContent = "import Web.View.PageComment.Index"}
+                , EnsureDirectory {directory = "Web/View/PageComment"}
+                , CreateFile {filePath = "Web/View/PageComment/New.hs", fileContent = "module Web.View.PageComment.New where\nimport Web.View.Prelude\n\ndata NewView = NewView { pageComment :: PageComment }\n\ninstance View NewView where\n    html NewView { .. } = [hsx|\n        {breadcrumb}\n        <h1>New PageComment</h1>\n        {renderForm pageComment}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"PageComments\" PageCommentsAction\n                , breadcrumbText \"New PageComment\"\n                ]\n\nrenderForm :: PageComment -> Html\nrenderForm pageComment = formFor pageComment [hsx|\n    \n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/PageComment.hs", fileContent = "import Web.View.PageComment.New"}
+                , EnsureDirectory {directory = "Web/View/PageComment"}
+                , CreateFile {filePath = "Web/View/PageComment/Show.hs", fileContent = "module Web.View.PageComment.Show where\nimport Web.View.Prelude\n\ndata ShowView = ShowView { pageComment :: PageComment }\n\ninstance View ShowView where\n    html ShowView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Show PageComment</h1>\n        <p>{pageComment}</p>\n\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                            [ breadcrumbLink \"PageComments\" PageCommentsAction\n                            , breadcrumbText \"Show PageComment\"\n                            ]"}
+                , AddImport {filePath = "Web/Controller/PageComment.hs", fileContent = "import Web.View.PageComment.Show"}
+                , EnsureDirectory {directory = "Web/View/PageComment"}
+                , CreateFile {filePath = "Web/View/PageComment/Edit.hs", fileContent = "module Web.View.PageComment.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { pageComment :: PageComment }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit PageComment</h1>\n        {renderForm pageComment}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"PageComments\" PageCommentsAction\n                , breadcrumbText \"Edit PageComment\"\n                ]\n\nrenderForm :: PageComment -> Html\nrenderForm pageComment = formFor pageComment [hsx|\n    \n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/PageComment.hs", fileContent = "import Web.View.PageComment.Edit"}
+                ]
+
+
+
+        it "should build a controller with name \"pageComment\"" do
+            let rawControllerName = "pageComment"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let pagination = False
+            let builtPlan = ControllerGenerator.buildPlan' schema (makeConfig controllerName modelName applicationName pagination)
+
+            builtPlan `shouldBe`
+                [ CreateFile {filePath = "Web/Controller/PageComment.hs", fileContent = "module Web.Controller.PageComment where\n\nimport Web.Controller.Prelude\nimport Web.View.PageComment.Index\nimport Web.View.PageComment.New\nimport Web.View.PageComment.Edit\nimport Web.View.PageComment.Show\n\ninstance Controller PageCommentController where\n    action PageCommentsAction = do\n        pageComment <- query @PageComment |> fetch\n        render IndexView { .. }\n\n    action NewPageCommentAction = do\n        let pageComment = newRecord\n        render NewView { .. }\n\n    action ShowPageCommentAction { pageCommentId } = do\n        pageComment <- fetch pageCommentId\n        render ShowView { .. }\n\n    action EditPageCommentAction { pageCommentId } = do\n        pageComment <- fetch pageCommentId\n        render EditView { .. }\n\n    action UpdatePageCommentAction { pageCommentId } = do\n        pageComment <- fetch pageCommentId\n        pageComment\n            |> buildPageComment\n            |> ifValid \\case\n                Left pageComment -> render EditView { .. }\n                Right pageComment -> do\n                    pageComment <- pageComment |> updateRecord\n                    setSuccessMessage \"PageComment updated\"\n                    redirectTo EditPageCommentAction { .. }\n\n    action CreatePageCommentAction = do\n        let pageComment = newRecord @PageComment\n        pageComment\n            |> buildPageComment\n            |> ifValid \\case\n                Left pageComment -> render NewView { .. } \n                Right pageComment -> do\n                    pageComment <- pageComment |> createRecord\n                    setSuccessMessage \"PageComment created\"\n                    redirectTo PageCommentsAction\n\n    action DeletePageCommentAction { pageCommentId } = do\n        pageComment <- fetch pageCommentId\n        deleteRecord pageComment\n        setSuccessMessage \"PageComment deleted\"\n        redirectTo PageCommentsAction\n\nbuildPageComment pageComment = pageComment\n    |> fill @'[]\n"}
+                , AppendToFile {filePath = "Web/Routes.hs", fileContent = "\ninstance AutoRoute PageCommentController\n\n"}
+                , AppendToFile {filePath = "Web/Types.hs", fileContent = "\ndata PageCommentController\n    = PageCommentsAction\n    | NewPageCommentAction\n    | ShowPageCommentAction { pageCommentId :: !(Id PageComment) }\n    | CreatePageCommentAction\n    | EditPageCommentAction { pageCommentId :: !(Id PageComment) }\n    | UpdatePageCommentAction { pageCommentId :: !(Id PageComment) }\n    | DeletePageCommentAction { pageCommentId :: !(Id PageComment) }\n    deriving (Eq, Show, Data)\n"}
+                , AppendToMarker {marker = "-- Controller Imports", filePath = "Web/FrontController.hs", fileContent = "import Web.Controller.PageComment"}
+                , AppendToMarker {marker = "-- Generator Marker", filePath = "Web/FrontController.hs", fileContent = "        , parseRoute @PageCommentController"}
+                , EnsureDirectory {directory = "Web/View/PageComment"}
+                , CreateFile {filePath = "Web/View/PageComment/Index.hs", fileContent = "module Web.View.PageComment.Index where\nimport Web.View.Prelude\n\ndata IndexView = IndexView { pageComment :: [PageComment] }\n\ninstance View IndexView where\n    html IndexView { .. } = [hsx|\n        {breadcrumb}\n\n        <h1>Index<a href={pathTo NewPageCommentAction} class=\"btn btn-primary ms-4\">+ New</a></h1>\n        <div class=\"table-responsive\">\n            <table class=\"table\">\n                <thead>\n                    <tr>\n                        <th>PageComment</th>\n                        <th></th>\n                        <th></th>\n                        <th></th>\n                    </tr>\n                </thead>\n                <tbody>{forEach pageComment renderPageComment}</tbody>\n            </table>\n            \n        </div>\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"PageComments\" PageCommentsAction\n                ]\n\nrenderPageComment :: PageComment -> Html\nrenderPageComment pageComment = [hsx|\n    <tr>\n        <td>{pageComment}</td>\n        <td><a href={ShowPageCommentAction pageComment.id}>Show</a></td>\n        <td><a href={EditPageCommentAction pageComment.id} class=\"text-muted\">Edit</a></td>\n        <td><a href={DeletePageCommentAction pageComment.id} class=\"js-delete text-muted\">Delete</a></td>\n    </tr>\n|]"}
+                , AddImport {filePath = "Web/Controller/PageComment.hs", fileContent = "import Web.View.PageComment.Index"}
+                , EnsureDirectory {directory = "Web/View/PageComment"}
+                , CreateFile {filePath = "Web/View/PageComment/New.hs", fileContent = "module Web.View.PageComment.New where\nimport Web.View.Prelude\n\ndata NewView = NewView { pageComment :: PageComment }\n\ninstance View NewView where\n    html NewView { .. } = [hsx|\n        {breadcrumb}\n        <h1>New PageComment</h1>\n        {renderForm pageComment}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"PageComments\" PageCommentsAction\n                , breadcrumbText \"New PageComment\"\n                ]\n\nrenderForm :: PageComment -> Html\nrenderForm pageComment = formFor pageComment [hsx|\n    \n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/PageComment.hs", fileContent = "import Web.View.PageComment.New"}
+                , EnsureDirectory {directory = "Web/View/PageComment"}
+                , CreateFile {filePath = "Web/View/PageComment/Show.hs", fileContent = "module Web.View.PageComment.Show where\nimport Web.View.Prelude\n\ndata ShowView = ShowView { pageComment :: PageComment }\n\ninstance View ShowView where\n    html ShowView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Show PageComment</h1>\n        <p>{pageComment}</p>\n\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                            [ breadcrumbLink \"PageComments\" PageCommentsAction\n                            , breadcrumbText \"Show PageComment\"\n                            ]"}
+                , AddImport {filePath = "Web/Controller/PageComment.hs", fileContent = "import Web.View.PageComment.Show"}
+                , EnsureDirectory {directory = "Web/View/PageComment"}
+                , CreateFile {filePath = "Web/View/PageComment/Edit.hs", fileContent = "module Web.View.PageComment.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { pageComment :: PageComment }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit PageComment</h1>\n        {renderForm pageComment}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"PageComments\" PageCommentsAction\n                , breadcrumbText \"Edit PageComment\"\n                ]\n\nrenderForm :: PageComment -> Html\nrenderForm pageComment = formFor pageComment [hsx|\n    \n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/PageComment.hs", fileContent = "import Web.View.PageComment.Edit"}
+                ]
+
+        it "should build a controller with name \"people\"" do
+            let rawControllerName = "people"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let pagination = False
+            let builtPlan = ControllerGenerator.buildPlan' schema (makeConfig controllerName modelName applicationName pagination)
+
+            builtPlan `shouldBe`
+                [ CreateFile {filePath = "Web/Controller/People.hs", fileContent = "module Web.Controller.People where\n\nimport Web.Controller.Prelude\nimport Web.View.People.Index\nimport Web.View.People.New\nimport Web.View.People.Edit\nimport Web.View.People.Show\n\ninstance Controller PeopleController where\n    action PeopleAction = do\n        people <- query @Person |> fetch\n        render IndexView { .. }\n\n    action NewPersonAction = do\n        let person = newRecord\n        render NewView { .. }\n\n    action ShowPersonAction { personId } = do\n        person <- fetch personId\n        render ShowView { .. }\n\n    action EditPersonAction { personId } = do\n        person <- fetch personId\n        render EditView { .. }\n\n    action UpdatePersonAction { personId } = do\n        person <- fetch personId\n        person\n            |> buildPerson\n            |> ifValid \\case\n                Left person -> render EditView { .. }\n                Right person -> do\n                    person <- person |> updateRecord\n                    setSuccessMessage \"Person updated\"\n                    redirectTo EditPersonAction { .. }\n\n    action CreatePersonAction = do\n        let person = newRecord @Person\n        person\n            |> buildPerson\n            |> ifValid \\case\n                Left person -> render NewView { .. } \n                Right person -> do\n                    person <- person |> createRecord\n                    setSuccessMessage \"Person created\"\n                    redirectTo PeopleAction\n\n    action DeletePersonAction { personId } = do\n        person <- fetch personId\n        deleteRecord person\n        setSuccessMessage \"Person deleted\"\n        redirectTo PeopleAction\n\nbuildPerson person = person\n    |> fill @'[\"name\", \"email\"]\n    |> validateField #name nonEmpty\n    |> validateField #email nonEmpty\n    |> validateField #email isEmail\n"}
+                , AppendToFile {filePath = "Web/Routes.hs", fileContent = "\ninstance AutoRoute PeopleController\n\n"}
+                , AppendToFile {filePath = "Web/Types.hs", fileContent = "\ndata PeopleController\n    = PeopleAction\n    | NewPersonAction\n    | ShowPersonAction { personId :: !(Id Person) }\n    | CreatePersonAction\n    | EditPersonAction { personId :: !(Id Person) }\n    | UpdatePersonAction { personId :: !(Id Person) }\n    | DeletePersonAction { personId :: !(Id Person) }\n    deriving (Eq, Show, Data)\n"}
+                , AppendToMarker {marker = "-- Controller Imports", filePath = "Web/FrontController.hs", fileContent = "import Web.Controller.People"}
+                , AppendToMarker {marker = "-- Generator Marker", filePath = "Web/FrontController.hs", fileContent = "        , parseRoute @PeopleController"}
+                , EnsureDirectory {directory = "Web/View/People"}
+                , CreateFile {filePath = "Web/View/People/Index.hs", fileContent = "module Web.View.People.Index where\nimport Web.View.Prelude\n\ndata IndexView = IndexView { people :: [Person] }\n\ninstance View IndexView where\n    html IndexView { .. } = [hsx|\n        {breadcrumb}\n\n        <h1>Index<a href={pathTo NewPersonAction} class=\"btn btn-primary ms-4\">+ New</a></h1>\n        <div class=\"table-responsive\">\n            <table class=\"table\">\n                <thead>\n                    <tr>\n                        <th>Name</th>\n                        <th>Email</th>\n                        <th></th>\n                        <th></th>\n                        <th></th>\n                    </tr>\n                </thead>\n                <tbody>{forEach people renderPerson}</tbody>\n            </table>\n            \n        </div>\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"People\" PeopleAction\n                ]\n\nrenderPerson :: Person -> Html\nrenderPerson person = [hsx|\n    <tr>\n        <td>{person.name}</td>\n        <td>{person.email}</td>\n        <td><a href={ShowPersonAction person.id}>Show</a></td>\n        <td><a href={EditPersonAction person.id} class=\"text-muted\">Edit</a></td>\n        <td><a href={DeletePersonAction person.id} class=\"js-delete text-muted\">Delete</a></td>\n    </tr>\n|]"}
+                , AddImport {filePath = "Web/Controller/People.hs", fileContent = "import Web.View.People.Index"}
+                , EnsureDirectory {directory = "Web/View/People"}
+                , CreateFile {filePath = "Web/View/People/New.hs", fileContent = "module Web.View.People.New where\nimport Web.View.Prelude\n\ndata NewView = NewView { person :: Person }\n\ninstance View NewView where\n    html NewView { .. } = [hsx|\n        {breadcrumb}\n        <h1>New Person</h1>\n        {renderForm person}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"People\" PeopleAction\n                , breadcrumbText \"New Person\"\n                ]\n\nrenderForm :: Person -> Html\nrenderForm person = formFor person [hsx|\n    {(textField #name)}\n    {(textField #email)}\n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/People.hs", fileContent = "import Web.View.People.New"}
+                , EnsureDirectory {directory = "Web/View/People"}
+                , CreateFile {filePath = "Web/View/People/Show.hs", fileContent = "module Web.View.People.Show where\nimport Web.View.Prelude\n\ndata ShowView = ShowView { person :: Person }\n\ninstance View ShowView where\n    html ShowView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Show Person</h1>\n        <dl>\n            <dt>Name</dt><dd>{person.name}</dd>\n            <dt>Email</dt><dd>{person.email}</dd>\n        </dl>\n\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                            [ breadcrumbLink \"People\" PeopleAction\n                            , breadcrumbText \"Show Person\"\n                            ]"}
+                , AddImport {filePath = "Web/Controller/People.hs", fileContent = "import Web.View.People.Show"}
+                , EnsureDirectory {directory = "Web/View/People"}
+                , CreateFile {filePath = "Web/View/People/Edit.hs", fileContent = "module Web.View.People.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { person :: Person }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit Person</h1>\n        {renderForm person}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"People\" PeopleAction\n                , breadcrumbText \"Edit Person\"\n                ]\n\nrenderForm :: Person -> Html\nrenderForm person = formFor person [hsx|\n    {(textField #name)}\n    {(textField #email)}\n    {submitButton}\n\n|]"}
+                , AddImport {filePath = "Web/Controller/People.hs", fileContent = "import Web.View.People.Edit"}
+                ]
+
+        it "should generate type-aware validation for a rich schema" do
+            let richSchema = [
+                        StatementCreateTable (table "projects") {
+                            columns = [
+                                (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                                , (col "title" PText) { notNull = True }
+                                , (col "is_active" PBoolean) { notNull = True }
+                                , (col "budget" PInt) { notNull = False }
+                                , (col "started_on" PDate) { notNull = False }
+                                , (col "contact_email" PText) { notNull = True }
+                                , (col "user_id" PUUID) { notNull = True }
+                            ]
+                            , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                            , constraints = [
+                                ForeignKeyConstraint { name = Nothing, columnName = "user_id", referenceTable = "users", referenceColumn = Nothing, onDelete = Nothing }
+                              ]
+                        }
+                        , AddConstraint { tableName = "projects", constraint = UniqueConstraint { name = Nothing, columnNames = ["contact_email"] }, deferrable = Nothing, deferrableType = Nothing }
+                    ]
+            let rawControllerName = "projects"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let pagination = False
+            let builtPlan = ControllerGenerator.buildPlan' richSchema (makeConfig controllerName modelName applicationName pagination)
+            let (CreateFile { fileContent = controllerContent }):_ = builtPlan
+            -- buildProject should have fill + validations
+            (cs controllerContent :: String) `shouldContain` "|> fill @'[\"title\", \"isActive\", \"budget\", \"startedOn\", \"contactEmail\", \"userId\"]"
+            (cs controllerContent :: String) `shouldContain` "|> validateField #title nonEmpty"
+            (cs controllerContent :: String) `shouldContain` "|> validateField #contactEmail nonEmpty"
+            (cs controllerContent :: String) `shouldContain` "|> validateField #contactEmail isEmail"
+            (cs controllerContent :: String) `shouldContain` "-- TODO: |> validateIsUnique #contactEmail"
+            -- budget is nullable int, should not get nonEmpty
+            (cs controllerContent :: String) `shouldNotContain` "validateField #budget nonEmpty"
+            -- is_active is boolean, should not get nonEmpty
+            (cs controllerContent :: String) `shouldNotContain` "validateField #isActive nonEmpty"
+
+        it "should build a controller with only Index and Show actions" do
+            let rawControllerName = "pages"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let config = (makeConfig controllerName modelName applicationName False)
+                    { ControllerGenerator.indexActionEnabled = True
+                    , ControllerGenerator.newActionEnabled = False
+                    , ControllerGenerator.showActionEnabled = True
+                    , ControllerGenerator.createActionEnabled = False
+                    , ControllerGenerator.editActionEnabled = False
+                    , ControllerGenerator.updateActionEnabled = False
+                    , ControllerGenerator.deleteActionEnabled = False
+                    }
+            let builtPlan = ControllerGenerator.buildPlan' schema config
+
+            -- Types.hs should only have Index and Show constructors
+            let [AppendToFile { fileContent = typesFileContent }] = builtPlan
+                    |> filter (\case AppendToFile { filePath } -> osPathToText filePath == "Web/Types.hs"; _ -> False)
+            let typesContent = cs typesFileContent :: String
+            typesContent `shouldContain` "PagesAction"
+            typesContent `shouldContain` "ShowPageAction"
+            typesContent `shouldNotContain` "NewPageAction"
+            typesContent `shouldNotContain` "CreatePageAction"
+            typesContent `shouldNotContain` "EditPageAction"
+            typesContent `shouldNotContain` "UpdatePageAction"
+            typesContent `shouldNotContain` "DeletePageAction"
+
+            -- Controller file should only have Index and Show action bodies
+            let (CreateFile { fileContent = controllerContent }):_ = builtPlan
+            (cs controllerContent :: String) `shouldContain` "action PagesAction"
+            (cs controllerContent :: String) `shouldContain` "action ShowPageAction"
+            (cs controllerContent :: String) `shouldNotContain` "action NewPageAction"
+            (cs controllerContent :: String) `shouldNotContain` "action CreatePageAction"
+            (cs controllerContent :: String) `shouldNotContain` "action EditPageAction"
+            (cs controllerContent :: String) `shouldNotContain` "action UpdatePageAction"
+            (cs controllerContent :: String) `shouldNotContain` "action DeletePageAction"
+            -- No buildPage helper since Create/Update are disabled
+            (cs controllerContent :: String) `shouldNotContain` "buildPage"
+            -- Only Index and Show view imports
+            (cs controllerContent :: String) `shouldContain` "import Web.View.Pages.Index"
+            (cs controllerContent :: String) `shouldContain` "import Web.View.Pages.Show"
+            (cs controllerContent :: String) `shouldNotContain` "import Web.View.Pages.New"
+            (cs controllerContent :: String) `shouldNotContain` "import Web.View.Pages.Edit"
+
+            -- Only Index and Show view files should be generated
+            let viewFiles = builtPlan
+                    |> filter (\case CreateFile { filePath } -> "View" `isInfixOf` osPathToText filePath; _ -> False)
+                    |> map (\(CreateFile { filePath }) -> osPathToText filePath)
+            viewFiles `shouldBe` ["Web/View/Pages/Index.hs", "Web/View/Pages/Show.hs"]
+
+        it "should not generate Id fields when table doesn't exist in schema" do
+            let emptySchema = []
+            let rawControllerName = "bars"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let pagination = False
+            let builtPlan = ControllerGenerator.buildPlan' emptySchema (makeConfig controllerName modelName applicationName pagination)
+
+            -- Types.hs should not contain barId or Id Bar
+            let [AppendToFile { fileContent = typesFileContent }] = builtPlan
+                    |> filter (\case AppendToFile { filePath } -> osPathToText filePath == "Web/Types.hs"; _ -> False)
+            let typesContent = cs typesFileContent :: String
+            typesContent `shouldNotContain` "barId"
+            typesContent `shouldNotContain` "Id Bar"
+            typesContent `shouldContain` "ShowBarAction\n"
+            typesContent `shouldContain` "EditBarAction\n"
+            typesContent `shouldContain` "UpdateBarAction\n"
+            typesContent `shouldContain` "DeleteBarAction\n"
+
+            -- Controller file should not contain { barId } destructuring or fetch barId
+            let (CreateFile { fileContent = controllerContent }):_ = builtPlan
+            (cs controllerContent :: String) `shouldNotContain` "barId"
+            (cs controllerContent :: String) `shouldNotContain` "fetch barId"
diff --git a/Test/IDE/CodeGeneration/JobGenerator.hs b/Test/IDE/CodeGeneration/JobGenerator.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/CodeGeneration/JobGenerator.hs
@@ -0,0 +1,97 @@
+{-|
+Module: Test.IDE.CodeGeneration.JobGenerator
+Copyright: (c) digitally induced GmbH, 2020
+-}
+module Test.IDE.CodeGeneration.JobGenerator where
+
+import Test.Hspec
+import IHP.Prelude
+import qualified IHP.IDE.CodeGen.JobGenerator as JobGenerator
+import IHP.IDE.CodeGen.Types
+
+tests = do
+    describe "Job Generator" do
+        it "should build a job with name \"CreateContainerJobs\"" do
+            let applicationName = "Web"
+            let tableName = "create_container_jobs"
+            let modelName = "CreateContainerJob"
+            let isFirstJobInApplication = False
+            let uuidFunction = "uuid_generate_v4"
+            let config = JobGenerator.JobConfig { .. }
+            let builtPlan = JobGenerator.buildPlan' config
+
+            builtPlan `shouldBe` [
+                  EnsureDirectory {directory = "Web/Job"}
+                , AppendToFile {filePath = "Application/Schema.sql", fileContent = "CREATE TABLE create_container_jobs (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    status JOB_STATUS DEFAULT 'job_status_not_started' NOT NULL,\n    last_error TEXT DEFAULT NULL,\n    attempts_count INT DEFAULT 0 NOT NULL,\n    locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,\n    locked_by UUID DEFAULT NULL,\n    run_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n);\n"}
+                , CreateFile {filePath = "Web/Job/CreateContainer.hs", fileContent = "module Web.Job.CreateContainer where\nimport Web.Controller.Prelude\n\ninstance Job CreateContainerJob where\n    perform CreateContainerJob { .. } = do\n        putStrLn \"Hello World!\"\n"}
+                , AddImport {filePath = "Web/Worker.hs", fileContent = "import Web.Job.CreateContainer"}
+                , AppendToMarker {marker = "-- Generator Marker", filePath = "Web/Worker.hs", fileContent = "        , worker @CreateContainerJob"}
+                ]
+
+        it "should build a job with suffix Job even when it's missing in the input" do
+            let applicationName = "Web"
+            let tableName = "create_container"
+            let modelName = "CreateContainer"
+            let isFirstJobInApplication = False
+            let uuidFunction = "uuid_generate_v4"
+            let config = JobGenerator.JobConfig { .. }
+            let builtPlan = JobGenerator.buildPlan' config
+
+            builtPlan `shouldBe` [
+                  EnsureDirectory {directory = "Web/Job"}
+                , AppendToFile {filePath = "Application/Schema.sql", fileContent = "CREATE TABLE create_container_jobs (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    status JOB_STATUS DEFAULT 'job_status_not_started' NOT NULL,\n    last_error TEXT DEFAULT NULL,\n    attempts_count INT DEFAULT 0 NOT NULL,\n    locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,\n    locked_by UUID DEFAULT NULL,\n    run_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n);\n"}
+                , CreateFile {filePath = "Web/Job/CreateContainer.hs", fileContent = "module Web.Job.CreateContainer where\nimport Web.Controller.Prelude\n\ninstance Job CreateContainerJob where\n    perform CreateContainerJob { .. } = do\n        putStrLn \"Hello World!\"\n"}
+                , AddImport {filePath = "Web/Worker.hs", fileContent = "import Web.Job.CreateContainer"}
+                , AppendToMarker {marker = "-- Generator Marker", filePath = "Web/Worker.hs", fileContent = "        , worker @CreateContainerJob"}
+                ]
+
+
+        it "should create Web/Worker.hs if it doesn't exist" do
+            let applicationName = "Web"
+            let tableName = "create_container_jobs"
+            let modelName = "CreateContainerJob"
+            let isFirstJobInApplication = True
+            let uuidFunction = "uuid_generate_v4"
+            let config = JobGenerator.JobConfig { .. }
+            let builtPlan = JobGenerator.buildPlan' config
+
+            builtPlan `shouldBe` [
+                  EnsureDirectory {directory = "Web/Job"}
+                , AppendToFile {filePath = "Application/Schema.sql", fileContent = "CREATE TABLE create_container_jobs (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    status JOB_STATUS DEFAULT 'job_status_not_started' NOT NULL,\n    last_error TEXT DEFAULT NULL,\n    attempts_count INT DEFAULT 0 NOT NULL,\n    locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,\n    locked_by UUID DEFAULT NULL,\n    run_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n);\n"}
+                , CreateFile {filePath = "Web/Job/CreateContainer.hs", fileContent = "module Web.Job.CreateContainer where\nimport Web.Controller.Prelude\n\ninstance Job CreateContainerJob where\n    perform CreateContainerJob { .. } = do\n        putStrLn \"Hello World!\"\n"}
+                , CreateFile {filePath = "Web/Worker.hs", fileContent = "module Web.Worker where\n\nimport IHP.Prelude\nimport Web.Types\nimport Generated.Types\nimport IHP.Job.Runner\nimport IHP.Job.Types\n\nimport Web.Job.CreateContainer\n\ninstance Worker WebApplication where\n    workers _ =\n        [ worker @CreateContainerJob\n        -- Generator Marker\n        ]\n"}
+                ]
+
+
+        it "should support other applications" do
+            let applicationName = "Admin"
+            let tableName = "create_container_jobs"
+            let modelName = "CreateContainerJob"
+            let isFirstJobInApplication = False
+            let uuidFunction = "uuid_generate_v4"
+            let config = JobGenerator.JobConfig { .. }
+            let builtPlan = JobGenerator.buildPlan' config
+
+            builtPlan `shouldBe` [
+                  EnsureDirectory {directory = "Admin/Job"}
+                , AppendToFile {filePath = "Application/Schema.sql", fileContent = "CREATE TABLE create_container_jobs (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    status JOB_STATUS DEFAULT 'job_status_not_started' NOT NULL,\n    last_error TEXT DEFAULT NULL,\n    attempts_count INT DEFAULT 0 NOT NULL,\n    locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,\n    locked_by UUID DEFAULT NULL,\n    run_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n);\n"}
+                , CreateFile {filePath = "Admin/Job/CreateContainer.hs", fileContent = "module Admin.Job.CreateContainer where\nimport Admin.Controller.Prelude\n\ninstance Job CreateContainerJob where\n    perform CreateContainerJob { .. } = do\n        putStrLn \"Hello World!\"\n"}
+                , AddImport {filePath = "Admin/Worker.hs", fileContent = "import Admin.Job.CreateContainer"}
+                , AppendToMarker {marker = "-- Generator Marker", filePath = "Admin/Worker.hs", fileContent = "        , worker @CreateContainerJob"}
+                ]
+
+        it "should create appropriate Worker.hs for other applications if it doesn't exist" do
+            let applicationName = "Admin"
+            let tableName = "create_container_jobs"
+            let modelName = "CreateContainerJob"
+            let isFirstJobInApplication = True
+            let uuidFunction = "uuid_generate_v4"
+            let config = JobGenerator.JobConfig { .. }
+            let builtPlan = JobGenerator.buildPlan' config
+
+            builtPlan `shouldBe` [
+                  EnsureDirectory {directory = "Admin/Job"}
+                , AppendToFile {filePath = "Application/Schema.sql", fileContent = "CREATE TABLE create_container_jobs (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n    status JOB_STATUS DEFAULT 'job_status_not_started' NOT NULL,\n    last_error TEXT DEFAULT NULL,\n    attempts_count INT DEFAULT 0 NOT NULL,\n    locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,\n    locked_by UUID DEFAULT NULL,\n    run_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n);\n"}
+                , CreateFile {filePath = "Admin/Job/CreateContainer.hs", fileContent = "module Admin.Job.CreateContainer where\nimport Admin.Controller.Prelude\n\ninstance Job CreateContainerJob where\n    perform CreateContainerJob { .. } = do\n        putStrLn \"Hello World!\"\n"}
+                , CreateFile {filePath = "Admin/Worker.hs", fileContent = "module Admin.Worker where\n\nimport IHP.Prelude\nimport Admin.Types\nimport Generated.Types\nimport IHP.Job.Runner\nimport IHP.Job.Types\n\nimport Admin.Job.CreateContainer\n\ninstance Worker AdminApplication where\n    workers _ =\n        [ worker @CreateContainerJob\n        -- Generator Marker\n        ]\n"}
+                ]
diff --git a/Test/IDE/CodeGeneration/MailGenerator.hs b/Test/IDE/CodeGeneration/MailGenerator.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/CodeGeneration/MailGenerator.hs
@@ -0,0 +1,38 @@
+{-|
+Module: Test.IDE.CodeGeneration.MailGenerator
+Copyright: (c) digitally induced GmbH, 2020
+-}
+module Test.IDE.CodeGeneration.MailGenerator where
+
+import Test.Hspec
+import IHP.Prelude
+import qualified IHP.IDE.CodeGen.MailGenerator as MailGenerator
+import IHP.IDE.CodeGen.Types
+import IHP.Postgres.Types
+
+
+
+tests = do
+    describe "Mail Generator Tests:" do
+        let schema = [
+                    StatementCreateTable (table "users") {
+                        columns = [
+                            (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                        ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        }
+                    ]
+        it "should build a mail with name \"PurchaseConfirmationMail\"" do
+            let mailName = "PurchaseConfirmationMail"
+            let rawControllerName = "Users"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let config = MailGenerator.MailConfig { .. }
+            let builtPlan = MailGenerator.buildPlan' schema config
+
+            builtPlan `shouldBe` [
+                  EnsureDirectory {directory = "Web/Mail/Users"}
+                , CreateFile {filePath = "Web/Mail/Users/PurchaseConfirmation.hs", fileContent = "module Web.Mail.Users.PurchaseConfirmation where\nimport Web.View.Prelude\nimport IHP.MailPrelude\n\ndata PurchaseConfirmationMail = PurchaseConfirmationMail { user :: User }\n\ninstance BuildMail PurchaseConfirmationMail where\n    subject = \"Subject\"\n    to PurchaseConfirmationMail { .. } = Address { addressName = Just \"Firstname Lastname\", addressEmail = \"fname.lname@example.com\" }\n    from = \"hi@example.com\"\n    html PurchaseConfirmationMail { .. } = [hsx|\n        Hello World\n    |]\n"}
+                , AddImport {filePath = "Web/Controller/Users.hs", fileContent = "import Web.Mail.Users.PurchaseConfirmation"}
+                ]
diff --git a/Test/IDE/CodeGeneration/MigrationGenerator.hs b/Test/IDE/CodeGeneration/MigrationGenerator.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/CodeGeneration/MigrationGenerator.hs
@@ -0,0 +1,1448 @@
+{-|
+Module: Test.IDE.CodeGeneration.MigrationGenerator
+Copyright: (c) digitally induced GmbH, 2021
+-}
+module Test.IDE.CodeGeneration.MigrationGenerator where
+
+import Test.Hspec
+import IHP.Prelude
+import IHP.IDE.CodeGen.MigrationGenerator
+import Data.String.Interpolate.IsString (i)
+import qualified Text.Megaparsec as Megaparsec
+import qualified IHP.Postgres.Parser as Parser
+import IHP.Postgres.Types
+
+tests = do
+    describe "MigrationGenerator" do
+        describe "diffSchemas" do
+            it "should handle an empty schema" do
+                diffSchemas [] [] `shouldBe` []
+
+            it "should handle a new table" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql ""
+
+                diffSchemas targetSchema actualSchema `shouldBe` targetSchema
+
+            it "should skip tables that are equals in both schemas" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = targetSchema
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+            it "should handle new columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|ALTER TABLE users ADD COLUMN email TEXT NOT NULL;|]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+
+            it "should handle multiple new columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    ALTER TABLE users ADD COLUMN name TEXT NOT NULL;
+                    ALTER TABLE users ADD COLUMN email TEXT NOT NULL;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle deleted columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    ALTER TABLE users DROP COLUMN email;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+
+            it "should handle renamed columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|ALTER TABLE users RENAME COLUMN name TO full_name;|]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should handle UNIQUE constraints added to columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT NOT NULL UNIQUE
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|ALTER TABLE users ADD CONSTRAINT "users_full_name_key" UNIQUE (full_name);|]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle changing default values for columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT DEFAULT 'new value' NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT DEFAULT 'old value' NOT NULL
+                    );
+                |]
+                let migration = sql [i|ALTER TABLE users ALTER COLUMN full_name SET DEFAULT 'new value';|]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle default values added to columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT DEFAULT 'value' NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|ALTER TABLE users ALTER COLUMN full_name SET DEFAULT 'value';|]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle default values removed from columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT DEFAULT 'value' NOT NULL
+                    );
+                |]
+                let migration = sql [i|ALTER TABLE users ALTER COLUMN full_name DROP DEFAULT;|]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle UNIQUE constraints removed from columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        full_name TEXT NOT NULL UNIQUE
+                    );
+                |]
+                let migration = sql [i|ALTER TABLE users DROP CONSTRAINT users_full_name_key;|]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle new enums" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL
+                    );
+                    CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle new enum values" do
+                let targetSchema = sql [i|
+                    CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
+                |]
+                let actualSchema = sql [i|
+                    CREATE TYPE mood AS ENUM ('sad', 'ok');
+                |]
+                let migration = sql [i|
+                    -- Commit the transaction previously started by IHP
+                    COMMIT;
+                    ALTER TYPE mood ADD VALUE IF NOT EXISTS 'happy';
+                    -- Restart the connection as IHP will also try to run it's own COMMIT
+                    BEGIN;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle a real world table" do
+                let targetSchema = sql [i|
+                    CREATE TABLE subscribe_to_convert_kit_tag_jobs (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        status JOB_STATUS DEFAULT 'job_status_not_started' NOT NULL,
+                        last_error TEXT DEFAULT NULL,
+                        attempts_count INT DEFAULT 0 NOT NULL,
+                        locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+                        locked_by UUID DEFAULT NULL,
+                        run_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        user_id UUID NOT NULL,
+                        tag_id INT NOT NULL
+                    );
+                |]
+
+                let actualSchema = sql [i|
+                    CREATE TABLE public.subscribe_to_convert_kit_tag_jobs (
+                        id uuid DEFAULT public.uuid_generate_v4() NOT NULL,
+                        created_at timestamp with time zone DEFAULT now() NOT NULL,
+                        updated_at timestamp with time zone DEFAULT now() NOT NULL,
+                        status public.job_status DEFAULT 'job_status_not_started'::public.job_status NOT NULL,
+                        last_error text,
+                        attempts_count integer DEFAULT 0 NOT NULL,
+                        locked_at timestamp with time zone,
+                        locked_by uuid,
+                        run_at timestamp with time zone DEFAULT now() NOT NULL,
+                        user_id uuid NOT NULL,
+                        tag_id integer NOT NULL
+                    );
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+            it "should not detect unspecified on delete behaviour as a change" do
+                let targetSchema = sql [i|
+                    ALTER TABLE subscriptions ADD CONSTRAINT subscriptions_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                |]
+
+                let actualSchema = sql [i|
+                    ALTER TABLE ONLY public.subscriptions ADD CONSTRAINT subscriptions_ref_user_id FOREIGN KEY (user_id) REFERENCES public.users(id);
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+            it "should not detect changes on case differences of enums" do
+                let targetSchema = sql [i|
+                    CREATE TYPE A AS ENUM ();
+                |]
+
+                let actualSchema = sql [i|
+                    CREATE TYPE a AS ENUM ();
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+            
+            it "should handle a deleted table" do
+                let targetSchema = sql ""
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    DROP TABLE users;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle a deleted enum" do
+                let targetSchema = sql ""
+                let actualSchema = sql [i|
+                    CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
+                |]
+                let migration = sql [i|
+                    DROP TYPE mood;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should handle a new indexes" do
+                let targetSchema = sql [i|
+                    CREATE INDEX users_index ON users (user_name);
+                |]
+                let actualSchema = sql ""
+                let migration = sql [i|
+                    CREATE INDEX users_index ON users (user_name);
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration 
+            
+            it "should handle deleted indexes" do
+                let targetSchema = sql ""
+                let actualSchema = sql [i|
+                    CREATE INDEX users_index ON users (user_name);
+                |]
+                let migration = sql [i|
+                    DROP INDEX users_index;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration 
+            
+            it "should handle columns that have been made nullable" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    ALTER TABLE users ALTER COLUMN email DROP NOT NULL;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should handle columns that have been made not nullable" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT
+                    );
+                |]
+                let migration = sql [i|
+                    ALTER TABLE users ALTER COLUMN email SET NOT NULL;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration 
+            
+            it "should handle table renames" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE profiles (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    ALTER TABLE profiles RENAME TO users;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration 
+            
+            it "should not do a rename if tables are different" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE profiles (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        email TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    DROP TABLE profiles;
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL
+                    );
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration 
+            
+            it "should handle new foreign keys" do
+                let targetSchema = sql [i|
+                    ALTER TABLE messages ADD CONSTRAINT messages_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                |]
+                let actualSchema = sql ""
+                let migration = sql [i|
+                    ALTER TABLE messages ADD CONSTRAINT messages_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should handle new foreign keys" do
+                let targetSchema = sql ""
+                let actualSchema = sql [i|
+                    ALTER TABLE ONLY public.messages ADD CONSTRAINT messages_ref_user_id FOREIGN KEY (user_id) REFERENCES public.users(id);
+                |]
+                let migration = sql [i|
+                    ALTER TABLE messages DROP CONSTRAINT messages_ref_user_id;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should handle new policies" do
+                let targetSchema = sql [i|
+                    CREATE POLICY "Users can manage their todos" ON todos USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                |]
+                let actualSchema = sql ""
+                let migration = sql [i|
+                    CREATE POLICY "Users can manage their todos" ON todos USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            
+            it "should handle deleted policies" do
+                let targetSchema = sql ""
+                let actualSchema = sql [i|
+                    CREATE POLICY "Users can manage their todos" ON todos USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                |]
+                let migration = sql [i|
+                    DROP POLICY "Users can manage their todos" ON todos;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should normalize primary keys" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        email TEXT NOT NULL,
+                        password_hash TEXT NOT NULL,
+                        locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+                        failed_login_attempts INT DEFAULT 0 NOT NULL,
+                        access_token TEXT DEFAULT NULL
+                    );
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL,
+                        body TEXT NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    --
+                    -- PostgreSQL database dump
+                    --
+
+                    -- Dumped from database version 14.0 (Debian 14.0-1.pgdg110+1)
+                    -- Dumped by pg_dump version 14beta1
+
+                    SET statement_timeout = 0;
+                    SET lock_timeout = 0;
+                    SET idle_in_transaction_session_timeout = 0;
+                    SET client_encoding = 'UTF8';
+                    SET standard_conforming_strings = on;
+                    SELECT pg_catalog.set_config('search_path', '', false);
+                    SET default_toast_compression = 'pglz';
+                    SET check_function_bodies = false;
+                    SET xmloption = content;
+                    SET client_min_messages = warning;
+                    SET row_security = off;
+
+                    --
+                    -- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -
+                    --
+
+                    CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
+
+
+                    --
+                    -- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: -
+                    --
+
+                    COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
+
+
+                    SET default_tablespace = '';
+
+                    SET default_table_access_method = heap;
+
+                    --
+                    -- Name: users; Type: TABLE; Schema: public; Owner: -
+                    --
+
+                    CREATE TABLE public.users (
+                        id uuid DEFAULT public.uuid_generate_v4() NOT NULL,
+                        email text NOT NULL,
+                        password_hash text NOT NULL,
+                        locked_at timestamp with time zone,
+                        failed_login_attempts integer DEFAULT 0 NOT NULL,
+                        access_token text
+                    );
+
+
+                    --
+                    -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+                    --
+
+                    ALTER TABLE ONLY public.users
+                        ADD CONSTRAINT users_pkey PRIMARY KEY (id);
+
+
+                    --
+                    -- PostgreSQL database dump complete
+                    --
+
+                |]
+                let migration = sql [i|
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL,
+                        body TEXT NOT NULL
+                    );
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            it "should generate statements in the right order" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        email TEXT NOT NULL,
+                        password_hash TEXT NOT NULL,
+                        locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+                        failed_login_attempts INT DEFAULT 0 NOT NULL,
+                        access_token TEXT DEFAULT NULL
+                    );
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL,
+                        body TEXT NOT NULL,
+                        user_id UUID NOT NULL
+                    );
+                    CREATE INDEX posts_user_id_index ON posts (user_id);
+                    ALTER TABLE posts ADD CONSTRAINT posts_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                |]
+                let actualSchema = sql [i|
+                    --
+                    -- PostgreSQL database dump
+                    --
+
+                    -- Dumped from database version 14.0 (Debian 14.0-1.pgdg110+1)
+                    -- Dumped by pg_dump version 14beta1
+
+                    SET statement_timeout = 0;
+                    SET lock_timeout = 0;
+                    SET idle_in_transaction_session_timeout = 0;
+                    SET client_encoding = 'UTF8';
+                    SET standard_conforming_strings = on;
+                    SELECT pg_catalog.set_config('search_path', '', false);
+                    SET default_toast_compression = 'pglz';
+                    SET check_function_bodies = false;
+                    SET xmloption = content;
+                    SET client_min_messages = warning;
+                    SET row_security = off;
+
+                    --
+                    -- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -
+                    --
+
+                    CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
+
+
+                    --
+                    -- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: -
+                    --
+
+                    COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
+
+                    SET default_tablespace = '';
+
+                    SET default_table_access_method = heap;
+
+                    --
+                    -- Name: users; Type: TABLE; Schema: public; Owner: -
+                    --
+
+                    CREATE TABLE public.users (
+                        id uuid DEFAULT public.uuid_generate_v4() NOT NULL,
+                        email text NOT NULL,
+                        password_hash text NOT NULL,
+                        locked_at timestamp with time zone,
+                        failed_login_attempts integer DEFAULT 0 NOT NULL,
+                        access_token text
+                    );
+
+
+                    --
+                    -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+                    --
+
+                    ALTER TABLE ONLY public.users
+                        ADD CONSTRAINT users_pkey PRIMARY KEY (id);
+
+                |]
+                let migration = sql [i|
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL,
+                        body TEXT NOT NULL,
+                        user_id UUID NOT NULL
+                    );
+                    CREATE INDEX posts_user_id_index ON posts (user_id);
+                    ALTER TABLE posts ADD CONSTRAINT posts_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+
+            it "should normalize unique constraints on columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        github_user_id INT DEFAULT NULL UNIQUE
+                    );
+                |]
+
+                let actualSchema = sql [i|
+                    CREATE TABLE public.users (
+                        id uuid DEFAULT public.uuid_generate_v4() NOT NULL,
+                        github_user_id integer
+                    );
+
+                    ALTER TABLE ONLY public.users ADD CONSTRAINT users_github_user_id_key UNIQUE (github_user_id);
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+
+            it "should normalize policy definitions" do
+                let targetSchema = sql [i|
+                    CREATE POLICY "Users can manage their project's migrations" ON migrations USING (EXISTS (SELECT 1 FROM projects WHERE projects.id = migrations.project_id)) WITH CHECK (EXISTS (SELECT 1 FROM projects WHERE projects.id = migrations.project_id));
+                |]
+
+                let actualSchema = sql [i|
+                    CREATE POLICY "Users can manage their project's migrations" ON public.migrations USING ((EXISTS ( SELECT 1
+                       FROM public.projects
+                      WHERE (projects.id = migrations.project_id)))) WITH CHECK ((EXISTS ( SELECT 1
+                       FROM public.projects
+                      WHERE (projects.id = migrations.project_id))));
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+            it "should normalize check constraints" do
+                let targetSchema = sql [i|
+                    CREATE TABLE public.a (
+                        id uuid DEFAULT public.uuid_generate_v4() NOT NULL
+                    );
+                    ALTER TABLE a ADD CONSTRAINT contact_email_or_url CHECK (contact_email IS NOT NULL OR source_url IS NOT NULL);
+                |]
+
+                let actualSchema = sql [i|
+                    CREATE TABLE public.a (
+                        id uuid DEFAULT public.uuid_generate_v4() NOT NULL,
+                        CONSTRAINT contact_email_or_url CHECK (((contact_email IS NOT NULL) OR (source_url IS NOT NULL)))
+                    );
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+            it "should normalize Bigserials" do
+                let targetSchema = sql [i|
+                    CREATE TABLE testserial (
+                        testcol BIGSERIAL NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE public.testserial (
+                        testcol bigint NOT NULL
+                    );
+
+                    CREATE SEQUENCE public.testserial_testcol_seq
+                        START WITH 1
+                        INCREMENT BY 1
+                        NO MINVALUE
+                        NO MAXVALUE
+                        CACHE 1;
+
+                    ALTER SEQUENCE public.testserial_testcol_seq OWNED BY public.testserial.testcol;
+                    ALTER TABLE ONLY public.testserial ALTER COLUMN testcol SET DEFAULT nextval('public.testserial_testcol_seq'::regclass);
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+            
+            it "should normalize Serials" do
+                let targetSchema = sql [i|
+                    CREATE TABLE testserial (
+                        testcol SERIAL NOT NULL
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE public.testserial (
+                        testcol int NOT NULL
+                    );
+
+                    CREATE SEQUENCE public.testserial_testcol_seq
+                        START WITH 1
+                        INCREMENT BY 1
+                        NO MINVALUE
+                        NO MAXVALUE
+                        CACHE 1;
+
+                    ALTER SEQUENCE public.testserial_testcol_seq OWNED BY public.testserial.testcol;
+                    ALTER TABLE ONLY public.testserial ALTER COLUMN testcol SET DEFAULT nextval('public.testserial_testcol_seq'::regclass);
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+            
+            it "should normalize index expressions" do
+                let targetSchema = sql [i|
+                    CREATE INDEX users_email_index ON users (LOWER(email));
+                |]
+                let actualSchema = sql [i|
+                    CREATE INDEX users_email_index ON public.users USING btree (lower(email));
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+            it "should not detect a difference between two functions when the only difference is between 'CREATE' and 'CREATE OR REPLACE'" do
+                let targetSchema = sql [i|
+                    CREATE OR REPLACE FUNCTION notify_did_insert_webrtc_connection() RETURNS TRIGGER AS $$
+                    BEGIN
+                        PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text);
+                        RETURN NEW;
+                    END;
+                    $$ language plpgsql;
+                |]
+                let actualSchema = sql [i|
+                    CREATE FUNCTION public.notify_did_insert_webrtc_connection() RETURNS trigger
+                        LANGUAGE plpgsql
+                        AS $$
+                    BEGIN
+                        PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text);
+                        RETURN NEW;
+                    END;
+                    $$;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+            
+            it "should normalize aliases in policies" do
+                let targetSchema = sql [i|
+                    CREATE POLICY "Users can see other users in their company" ON users USING (company_id = (SELECT users.company_id FROM users WHERE users.id = ihp_user_id()));
+                |]
+                let actualSchema = sql [i|
+                    CREATE POLICY "Users can see other users in their company" ON public.users USING ((company_id = ( SELECT users_1.company_id
+                       FROM public.users users_1
+                      WHERE (users_1.id = public.ihp_user_id()))));
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` []
+            
+            it "should handle implicitly deleted indexes and constraints" do
+                let targetSchema = sql [i|
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE projects (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        user_id UUID NOT NULL
+                    );
+                    CREATE INDEX projects_name_index ON projects (name);
+                    ALTER TABLE projects ADD CONSTRAINT projects_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE;
+                |]
+                let migration = sql [i|
+                    DROP TABLE projects;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should run 'ALTER TYPE .. ADD VALUE ..' outside of a transaction" do
+                let targetSchema = sql [i|
+                    CREATE TABLE a();
+                    CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
+                    CREATE TABLE b();
+                |]
+                let actualSchema = sql [i|
+                    CREATE TYPE mood AS ENUM ('sad', 'ok');
+                |]
+                let migration = sql [i|
+                    -- Commit the transaction previously started by IHP
+                    COMMIT;
+                    ALTER TYPE mood ADD VALUE IF NOT EXISTS 'happy';
+                    -- Restart the connection as IHP will also try to run it's own COMMIT
+                    BEGIN;
+                    CREATE TABLE a();
+                    CREATE TABLE b();
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should run not generate a default value for a generated column" do
+                let targetSchema = sql [i|
+                    CREATE TABLE products (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        description TEXT NOT NULL,
+                        sku TEXT NOT NULL,
+                        text_search TSVECTOR GENERATED ALWAYS AS 
+                            ( setweight(to_tsvector('english', sku), 'A') ||
+                              setweight(to_tsvector('english', name), 'B') ||
+                              setweight(to_tsvector('english', description), 'C')
+                            ) STORED
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE products (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        description TEXT NOT NULL,
+                        sku TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    ALTER TABLE products ADD COLUMN text_search TSVECTOR GENERATED ALWAYS AS (setweight(to_tsvector('english', sku), 'A') || setweight(to_tsvector('english', name), 'B') || setweight(to_tsvector('english', description), 'C')) STORED;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should normalize generated columns" do
+                let targetSchema = sql [i|
+                    CREATE TABLE products (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        name TEXT NOT NULL,
+                        description TEXT NOT NULL,
+                        sku TEXT NOT NULL,
+                        text_search TSVECTOR GENERATED ALWAYS AS 
+                            ( setweight(to_tsvector('english', sku), 'A') ||
+                              setweight(to_tsvector('english', name), 'B') ||
+                              setweight(to_tsvector('english', description), 'C')
+                            ) STORED
+                    );
+                |]
+                let actualSchema = sql [i|
+                    CREATE TABLE public.products (
+                        id uuid DEFAULT public.uuid_generate_v4() NOT NULL,
+                        name text NOT NULL,
+                        description text NOT NULL,
+                        sku text NOT NULL,
+                        text_search tsvector GENERATED ALWAYS AS (((setweight(to_tsvector('english'::regconfig, sku), 'A'::"char") || setweight(to_tsvector('english'::regconfig, name), 'B'::"char")) || setweight(to_tsvector('english'::regconfig, description), 'C'::"char"))) STORED
+                    );
+                |]
+                let migration = sql [i|
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should not detect changes if the LANGUAGE is in difference casing" do
+                let targetSchema = sql [trimming|
+                    CREATE FUNCTION ihp_user_id() RETURNS UUID AS $$$$
+                        SELECT NULLIF(current_setting('rls.ihp_user_id'), '')::uuid;
+                    $$$$ LANGUAGE SQL;
+
+                    CREATE FUNCTION set_updated_at_to_now() RETURNS TRIGGER AS $$$$
+                    BEGIN
+                        NEW.updated_at = NOW();
+                        RETURN NEW;
+                    END;
+                    $$$$ language plpgsql;
+                |]
+                let actualSchema = sql [trimming|
+                    --
+                    -- Name: ihp_user_id(); Type: FUNCTION; Schema: public; Owner: -
+                    --
+
+                    CREATE FUNCTION public.ihp_user_id() RETURNS uuid
+                        LANGUAGE sql
+                        AS $$$$
+                        SELECT NULLIF(current_setting('rls.ihp_user_id'), '')::uuid;
+                    $$$$;
+
+                    CREATE FUNCTION public.set_updated_at_to_now() RETURNS trigger
+                        LANGUAGE plpgsql
+                        AS $$$$
+                    BEGIN
+                        NEW.updated_at = NOW();
+                        RETURN NEW;
+                    END;
+                    $$$$;
+                |]
+                let migration = sql [i|
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should not try dropping an index after already droping a column" do
+                let targetSchema = sql [trimming|
+                    CREATE TABLE tasks (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        user_id UUID DEFAULT ihp_user_id() NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
+                    );
+                    CREATE INDEX tasks_updated_at_index ON tasks (updated_at);
+                |]
+                let actualSchema = sql [trimming|
+                    CREATE TABLE tasks (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        user_id UUID DEFAULT ihp_user_id() NOT NULL,
+                        created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
+                    );
+                    CREATE INDEX tasks_created_at_index ON tasks (created_at);
+                    CREATE INDEX tasks_updated_at_index ON tasks (updated_at);
+                |]
+                let migration = sql [i|
+                    ALTER TABLE tasks DROP COLUMN created_at;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "ignore auto generated 'notify_...' functions" do
+                let targetSchema = sql [trimming|
+                |]
+                let actualSchema = sql [trimming|
+                    CREATE FUNCTION public.notify_did_change_todos() RETURNS trigger
+                        LANGUAGE plpgsql
+                        AS $$$$
+                            BEGIN
+                                CASE TG_OP
+                                WHEN ''UPDATE'' THEN
+                                    PERFORM pg_notify(
+                                        ''did_change_todos'',
+                                        json_build_object(
+                                          ''UPDATE'', NEW.id::text,
+                                          ''CHANGESET'', (
+                                                SELECT json_agg(row_to_json(t))
+                                                FROM (
+                                                      SELECT pre.key AS "col", post.value AS "new"
+                                                      FROM jsonb_each(to_jsonb(OLD)) AS pre
+                                                      CROSS JOIN jsonb_each(to_jsonb(NEW)) AS post
+                                                      WHERE pre.key = post.key AND pre.value IS DISTINCT FROM post.value
+                                                ) t
+                                          )
+                                        )::text
+                                    );
+                                WHEN ''DELETE'' THEN
+                                    PERFORM pg_notify(
+                                        ''did_change_todos'',
+                                        (json_build_object(''DELETE'', OLD.id)::text)
+                                    );
+                                WHEN ''INSERT'' THEN
+                                    PERFORM pg_notify(
+                                        ''did_change_todos'',
+                                        json_build_object(''INSERT'', NEW.id)::text
+                                    );
+                                END CASE;
+                                RETURN new;
+                            END;
+                        $$$$;
+                |]
+                let migration = sql [i|
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should normalize unique constraint names with multiple columns" do
+                let targetSchema = sql $ cs [plain|
+                    ALTER TABLE days ADD UNIQUE (category_id, date);
+                |]
+                let actualSchema = sql $ cs [plain|
+                    ALTER TABLE ONLY public.days ADD CONSTRAINT days_category_id_date_key UNIQUE (category_id, date);
+                |]
+                let migration = sql [i|
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should not detect changes between functions where only the whitespace is different" do
+                let targetSchema = sql $ cs [plain|
+CREATE FUNCTION set_updated_at_to_now() RETURNS TRIGGER AS $$
+BEGIN
+    NEW.updated_at = NOW();
+    RETURN NEW;
+END;
+$$ language PLPGSQL;
+                |]
+                let actualSchema = sql $ cs [plain|
+                    
+CREATE FUNCTION public.set_updated_at_to_now() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+    BEGIN
+        NEW.updated_at = NOW();
+        RETURN NEW;
+    END;
+    $$;
+                |]
+                let migration = sql [i|
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should replace functions if the body has changed" do
+                let targetSchema = sql $ cs [plain|
+CREATE FUNCTION a() RETURNS TRIGGER AS $$
+BEGIN
+    hello_world();
+END;
+$$ language PLPGSQL;
+                |]
+                let actualSchema = sql $ cs [plain|
+                    
+CREATE FUNCTION public.a() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+    BEGIN
+        RETURN NEW;
+    END;
+    $$;
+                |]
+                let migration = sql [i|
+CREATE OR REPLACE FUNCTION a() RETURNS TRIGGER AS $$BEGIN
+    hello_world();
+END;$$ language PLPGSQL;|]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should normalize qualified identifiers in policy expressions" do
+                -- https://github.com/digitallyinduced/ihp/issues/1480
+                let targetSchema = sql $ cs [plain|
+                    CREATE POLICY "Users can manage servers they have access to" ON servers USING (servers.user_id = ihp_user_id() OR (EXISTS (SELECT 1 FROM public.user_server_access WHERE user_server_access.user_id = ihp_user_id() AND user_server_access.server_id = servers.id)));
+                |]
+                let actualSchema = sql $ cs [plain|
+                    --
+                    -- Name: servers Users can manage servers they have access to; Type: POLICY; Schema: public; Owner: -
+                    --
+
+                    CREATE POLICY "Users can manage servers they have access to" ON public.servers USING (((user_id = public.ihp_user_id()) OR (EXISTS ( SELECT 1
+                       FROM public.user_server_access
+                      WHERE ((user_server_access.user_id = public.ihp_user_id()) AND (user_server_access.server_id = servers.id))))));
+                |]
+                let migration = sql [i|
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should work with IN expressions" do
+                let targetSchema = sql $ cs [plain|
+CREATE POLICY "Users can read and edit their own record" ON public.users USING ((id IN ( SELECT users_1.id
+   FROM public.users users_1
+  WHERE ((users_1.id = public.ihp_user_id()) OR (users_1.user_role = 'admin'::text))))) WITH CHECK ((id = public.ihp_user_id()));
+                |]
+                let actualSchema = sql $ cs [plain|
+                |]
+                let migration = sql [i|
+CREATE POLICY "Users can read and edit their own record" ON public.users USING ((id IN ( SELECT id
+   FROM public.users
+  WHERE ((id = public.ihp_user_id()) OR (user_role = 'admin'))))) WITH CHECK ((id = public.ihp_user_id()));
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            it "should handle indexes with coalesce" do
+                -- https://github.com/digitallyinduced/ihp/issues/1451
+                let targetSchema = sql "CREATE UNIQUE INDEX user_invite_uniqueness ON user_invites (organization_id, email, coalesce(expires_at, '0001-01-01 01:01:01-04'));"
+                let actualSchema = sql ""
+                let migration = sql [i|
+                    CREATE UNIQUE INDEX user_invite_uniqueness ON user_invites (organization_id, email, coalesce(expires_at, '0001-01-01 01:01:01-04'));
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration 
+
+            it "should handle complex renames" do
+                -- See https://github.com/digitallyinduced/thin-backend/issues/66
+                let targetSchema = sql $ cs [plain|
+                    CREATE FUNCTION set_updated_at_to_now() RETURNS TRIGGER AS $$
+                    BEGIN
+                        NEW.updated_at = NOW();
+                        RETURN NEW;
+                    END;
+                    $$ language plpgsql;
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        email TEXT NOT NULL,
+                        password_hash TEXT NOT NULL,
+                        locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+                        failed_login_attempts INT DEFAULT 0 NOT NULL,
+                        access_token TEXT DEFAULT NULL,
+                        confirmation_token TEXT DEFAULT NULL,
+                        is_confirmed BOOLEAN DEFAULT false NOT NULL
+                    );
+                    CREATE POLICY "Users can read their own record" ON users USING (id = ihp_user_id()) WITH CHECK (false);
+                    ALTER TABLE users ENABLE ROW LEVEL SECURITY;
+                    CREATE TABLE artefacts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        user_id UUID DEFAULT ihp_user_id() NOT NULL
+                    );
+                    CREATE INDEX artefacts_created_at_index ON artefacts (created_at);
+                    CREATE TRIGGER update_artefacts_updated_at BEFORE UPDATE ON artefacts FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
+                    CREATE INDEX artefacts_user_id_index ON artefacts (user_id);
+                    ALTER TABLE artefacts ADD CONSTRAINT artefacts_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                    ALTER TABLE artefacts ENABLE ROW LEVEL SECURITY;
+                    CREATE POLICY "Users can manage their artefacts" ON artefacts USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                |]
+                let actualSchema = sql $ cs [plain|
+                    CREATE FUNCTION set_updated_at_to_now() RETURNS TRIGGER AS $$
+                    BEGIN
+                        NEW.updated_at = NOW();
+                        RETURN NEW;
+                    END;
+                    $$ language plpgsql;
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        email TEXT NOT NULL,
+                        password_hash TEXT NOT NULL,
+                        locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+                        failed_login_attempts INT DEFAULT 0 NOT NULL,
+                        access_token TEXT DEFAULT NULL,
+                        confirmation_token TEXT DEFAULT NULL,
+                        is_confirmed BOOLEAN DEFAULT false NOT NULL
+                    );
+                    CREATE POLICY "Users can read their own record" ON users USING (id = ihp_user_id()) WITH CHECK (false);
+                    ALTER TABLE users ENABLE ROW LEVEL SECURITY;
+                    CREATE TABLE media (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        user_id UUID DEFAULT ihp_user_id() NOT NULL
+                    );
+                    CREATE INDEX media_created_at_index ON media (created_at);
+                    CREATE TRIGGER update_media_updated_at BEFORE UPDATE ON media FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
+                    CREATE INDEX media_user_id_index ON media (user_id);
+                    ALTER TABLE media ADD CONSTRAINT media_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                    ALTER TABLE media ENABLE ROW LEVEL SECURITY;
+                    CREATE POLICY "Users can manage their media" ON media USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+
+                |]
+                let migration = sql [i|
+                    ALTER TABLE media RENAME TO artefacts;
+                    
+                    DROP INDEX media_created_at_index;
+                    DROP TRIGGER update_media_updated_at ON media;
+                    DROP INDEX media_user_id_index;
+
+                    ALTER TABLE artefacts DROP CONSTRAINT media_ref_user_id;
+                    DROP POLICY "Users can manage their media" ON artefacts;
+                    
+                    CREATE INDEX artefacts_created_at_index ON artefacts (created_at);
+                    CREATE TRIGGER update_artefacts_updated_at BEFORE UPDATE ON artefacts FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
+                    CREATE INDEX artefacts_user_id_index ON artefacts (user_id);
+                    ALTER TABLE artefacts ADD CONSTRAINT artefacts_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                    ALTER TABLE artefacts ENABLE ROW LEVEL SECURITY;
+                    CREATE POLICY "Users can manage their artefacts" ON artefacts USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should delete policies when the column is deleted" do
+                -- https://github.com/digitallyinduced/ihp/issues/1480
+                let targetSchema = sql $ cs [plain|
+                    CREATE TABLE artefacts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
+                    );
+                    ALTER TABLE artefacts ENABLE ROW LEVEL SECURITY;
+                |]
+                let actualSchema = sql $ cs [plain|
+                    CREATE TABLE artefacts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
+                        user_id UUID DEFAULT ihp_user_id() NOT NULL
+                    );
+                    ALTER TABLE artefacts ENABLE ROW LEVEL SECURITY;
+                    CREATE POLICY "Users can manage their artefacts" ON artefacts USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                |]
+                let migration = sql [i|
+                    ALTER TABLE artefacts DROP COLUMN user_id;
+                    DROP POLICY "Users can manage their artefacts" ON artefacts;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should not explicitly delete policies when the table is deleted" do
+                -- https://github.com/digitallyinduced/thin-backend/issues/69
+                let actualSchema = sql $ cs [plain|
+                    CREATE TABLE tests (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        user_id UUID DEFAULT ihp_user_id() NOT NULL
+                    );
+                    CREATE INDEX tests_user_id_index ON tests (user_id);
+                    ALTER TABLE tests ADD CONSTRAINT tests_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                    ALTER TABLE tests ENABLE ROW LEVEL SECURITY;
+                    CREATE POLICY "Users can manage their tests" ON tests USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                |]
+                let targetSchema = []
+                let migration = sql [i|
+                    DROP TABLE tests;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should ignore the schema_migrations table" do
+                let actualSchema = sql $ cs [plain|
+                    CREATE TABLE schema_migrations (revision BIGINT NOT NULL UNIQUE);
+                |]
+                let targetSchema = []
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should ignore the large_pg_notifications table" do
+                let actualSchema = sql $ cs [plain|
+                    CREATE UNLOGGED TABLE large_pg_notifications (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        payload TEXT DEFAULT null,
+                        created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
+                    );
+                    CREATE INDEX large_pg_notifications_created_at_index ON large_pg_notifications (created_at);
+                |]
+                let targetSchema = []
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should not see a diff between those two" do
+                -- https://github.com/digitallyinduced/ihp/issues/1628
+                let actualSchema = sql $ cs [trimming|
+                    CREATE FUNCTION set_updated_at_to_now() RETURNS TRIGGER AS $$$$BEGIN
+                        NEW.updated_at = NOW();
+                        RETURN NEW;
+                    END;$$$$ language PLPGSQL;
+                |]
+                let targetSchema = sql $ cs [trimming|
+                    CREATE FUNCTION public.set_updated_at_to_now() RETURNS trigger
+                        LANGUAGE plpgsql
+                        AS $$$$BEGIN
+                            NEW.updated_at = NOW();
+                            RETURN NEW;
+                        END;$$$$;
+                |]
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should normalize function body whitespace" do
+                -- https://github.com/digitallyinduced/ihp/issues/1628
+                let (Just fn) = head $ sql $ cs [trimming|
+                    CREATE FUNCTION public.set_updated_at_to_now() RETURNS trigger
+                        LANGUAGE plpgsql
+                        AS $$$$BEGIN
+                            NEW.updated_at = NOW();
+                            RETURN NEW;
+                        END;$$$$;
+                |]
+
+                (normalizeStatement fn) `shouldBe` [(function "set_updated_at_to_now")
+                    { functionBody = "BEGIN\n    NEW.updated_at = NOW();\n    RETURN NEW;\nEND;"
+                    , language = "PLPGSQL"
+                    }]
+
+            it "should delete the updated_at trigger when the updated_at column is deleted" do
+                -- https://github.com/digitallyinduced/ihp/issues/1630
+                let actualSchema = sql $ cs [plain|
+                    CREATE FUNCTION set_updated_at_to_now() RETURNS TRIGGER AS $$
+                    BEGIN
+                        NEW.updated_at = NOW();
+                        RETURN NEW;
+                    END;
+                    $$ language plpgsql;
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL,
+                        updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
+                    );
+                    CREATE TRIGGER update_posts_updated_at BEFORE UPDATE ON posts FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
+                |]
+                let targetSchema = sql $ cs [plain|
+                    CREATE FUNCTION set_updated_at_to_now() RETURNS TRIGGER AS $$
+                    BEGIN
+                        NEW.updated_at = NOW();
+                        RETURN NEW;
+                    END;
+                    $$ language plpgsql;
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL
+                    );
+                |]
+                let migration = sql [i|
+                    ALTER TABLE posts DROP COLUMN updated_at;
+                    DROP TRIGGER update_posts_updated_at ON posts;
+                |]
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should ignore did_update_.. triggers by IHP.PGListener" do
+                let actualSchema = sql $ cs [plain|
+                    CREATE TRIGGER did_update_plans AFTER UPDATE ON public.plans FOR EACH ROW EXECUTE FUNCTION public.notify_did_change_plans();
+                    CREATE TRIGGER did_insert_offices AFTER INSERT ON public.offices FOR EACH STATEMENT EXECUTE FUNCTION public.notify_did_change_offices();
+                    CREATE TRIGGER did_delete_company_profiles AFTER DELETE ON public.company_profiles FOR EACH STATEMENT EXECUTE FUNCTION public.notify_did_change_company_profiles();
+                |]
+                let targetSchema = []
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should ignore ar_did_update_.. triggers by IHP.AutoRefresh" do
+                let actualSchema = sql $ cs [plain|
+                    CREATE TRIGGER ar_did_update_plans AFTER UPDATE ON public.plans FOR EACH ROW EXECUTE FUNCTION public.notify_did_change_plans();
+                    CREATE TRIGGER ar_did_insert_offices AFTER INSERT ON public.offices FOR EACH STATEMENT EXECUTE FUNCTION public.notify_did_change_offices();
+                    CREATE TRIGGER ar_did_delete_company_profiles AFTER DELETE ON public.company_profiles FOR EACH STATEMENT EXECUTE FUNCTION public.notify_did_change_company_profiles();
+                |]
+                let targetSchema = []
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should deal with truncated identifiers" do
+                let actualSchema = sql $ cs [plain|
+                    CREATE POLICY "Users can manage the prepare_context_jobs if they can see the C" ON public.prepare_context_jobs USING ((EXISTS ( SELECT 1
+                       FROM public.contexts
+                      WHERE (contexts.id = prepare_context_jobs.context_id)))) WITH CHECK ((EXISTS ( SELECT 1
+                       FROM public.contexts
+                      WHERE (contexts.id = prepare_context_jobs.context_id))));
+                |]
+                let targetSchema = sql $ cs [plain|
+                    CREATE POLICY "Users can manage the prepare_context_jobs if they can see the Context" ON prepare_context_jobs USING (EXISTS (SELECT 1 FROM public.contexts WHERE contexts.id = prepare_context_jobs.context_id)) WITH CHECK (EXISTS (SELECT 1 FROM public.contexts WHERE contexts.id = prepare_context_jobs.context_id));
+                |]
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should truncate very long constraint names" do
+                let actualSchema = sql $ cs [plain|
+                    ALTER TABLE organization_num_employees_ranges ADD CONSTRAINT organization_num_employees_ranges_ref_prospect_search_request_id FOREIGN KEY (prospect_search_request_id) REFERENCES prospect_search_requests (id) ON DELETE NO ACTION;
+                |]
+                let targetSchema = sql $ cs [plain|
+                    ALTER TABLE organization_num_employees_ranges ADD CONSTRAINT organization_num_employees_ranges_ref_prospect_search_request_i FOREIGN KEY (prospect_search_request_id) REFERENCES prospect_search_requests (id) ON DELETE NO ACTION;
+                |]
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should deal with nested SELECT expressions inside a policy" do
+                let actualSchema = sql $ cs [plain|
+                    CREATE POLICY "Allow users to see their own company" ON public.companies USING ((id = ( SELECT users.company_id
+                       FROM public.users
+                      WHERE (users.id = public.ihp_user_id())))) WITH CHECK (false);
+                |]
+                let targetSchema = sql $ cs [plain|
+                    CREATE POLICY "Allow users to see their own company" ON companies USING (id = (SELECT company_id FROM users WHERE users.id = ihp_user_id())) WITH CHECK (false);
+                |]
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+            
+            it "should deal with complex nested SELECT expressions inside a policy" do
+                -- Tricky part is the `projects.id = ads.project_id` here
+                -- It needs to be unwrapped to `id = project_id` correctly
+                let actualSchema = sql $ cs [plain|
+                    CREATE POLICY "Users can manage ads if they can access the project" ON public.ads USING ((EXISTS ( SELECT projects.id
+                       FROM public.projects
+                      WHERE (projects.id = ads.project_id)))) WITH CHECK ((EXISTS ( SELECT projects.id
+                       FROM public.projects
+                      WHERE (projects.id = ads.project_id))));
+                |]
+                let targetSchema = sql $ cs [plain|
+                    CREATE POLICY "Users can manage ads if they can access the project" ON ads USING (EXISTS (SELECT id FROM projects WHERE id = project_id)) WITH CHECK (EXISTS (SELECT id FROM projects WHERE id = project_id));
+                |]
+                let migration = []
+
+                diffSchemas targetSchema actualSchema `shouldBe` migration
+
+            it "should normalize InArrayExpression in check constraints" do
+                -- Ensures normalizeExpression handles InArrayExpression (tuple literals like ('a', 'b'))
+                -- This pattern appears more frequently in PostgreSQL 17's pg_dump output
+                let targetSchema = sql $ cs [plain|
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        status TEXT NOT NULL
+                    );
+                    ALTER TABLE posts ADD CONSTRAINT posts_status_check CHECK (status IN ('draft', 'published', 'archived'));
+                |]
+                let actualSchema = sql $ cs [plain|
+                    CREATE TABLE public.posts (
+                        id uuid DEFAULT public.uuid_generate_v4() NOT NULL,
+                        status text NOT NULL,
+                        CONSTRAINT posts_status_check CHECK ((status IN ('draft', 'published', 'archived')))
+                    );
+                |]
+                diffSchemas targetSchema actualSchema `shouldBe` []
+
+sql :: Text -> [Statement]
+sql code = case Megaparsec.runParser Parser.parseDDL "" code of
+    Left parsingFailed -> error (cs $ Megaparsec.errorBundlePretty parsingFailed)
+    Right r -> r
diff --git a/Test/IDE/CodeGeneration/ViewGenerator.hs b/Test/IDE/CodeGeneration/ViewGenerator.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/CodeGeneration/ViewGenerator.hs
@@ -0,0 +1,186 @@
+{-|
+Module: Test.IDE.CodeGeneration.ViewGenerator
+Copyright: (c) digitally induced GmbH, 2020
+-}
+module Test.IDE.CodeGeneration.ViewGenerator where
+
+import Test.Hspec
+import IHP.Prelude
+import qualified IHP.IDE.CodeGen.ViewGenerator as ViewGenerator
+import IHP.IDE.CodeGen.Types
+import IHP.Postgres.Types
+
+
+
+tests = do
+    describe "View Generator Tests:" do
+        let schema = [
+                    StatementCreateTable (table "pages") {
+                        columns = [
+                            (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                        ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        }
+                    ]
+        it "should build a view with name \"EditView\"" do
+            let rawViewName = "EditView"
+            let viewName = tableNameToViewName rawViewName
+            let rawControllerName = "Pages"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let paginationEnabled = False
+            let config = ViewGenerator.ViewConfig { .. }
+            let builtPlan = ViewGenerator.buildPlan' schema config
+
+            builtPlan `shouldBe`
+                [ EnsureDirectory {directory = "Web/View/Pages"},CreateFile {filePath = "Web/View/Pages/Edit.hs", fileContent = "module Web.View.Pages.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { page :: Page }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit Page</h1>\n        {renderForm page}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                , breadcrumbText \"Edit Page\"\n                ]\n\nrenderForm :: Page -> Html\nrenderForm page = formFor page [hsx|\n    \n    {submitButton}\n\n|]"},AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.Edit"}
+                ]
+
+
+
+        it "should build a view with name \"edit_view\"" do
+            let rawViewName = "edit_view"
+            let viewName = tableNameToViewName rawViewName
+            let rawControllerName = "Pages"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let paginationEnabled = False
+            let config = ViewGenerator.ViewConfig { .. }
+            let builtPlan = ViewGenerator.buildPlan' schema config
+
+            builtPlan `shouldBe`
+                [ EnsureDirectory {directory = "Web/View/Pages"},CreateFile {filePath = "Web/View/Pages/Edit.hs", fileContent = "module Web.View.Pages.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { page :: Page }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit Page</h1>\n        {renderForm page}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                , breadcrumbText \"Edit Page\"\n                ]\n\nrenderForm :: Page -> Html\nrenderForm page = formFor page [hsx|\n    \n    {submitButton}\n\n|]"},AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.Edit"}
+                ]
+
+
+        it "should build a view with name \"editView\"" do
+            let rawViewName = "editView"
+            let viewName = tableNameToViewName rawViewName
+            let rawControllerName = "Pages"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let paginationEnabled = False
+            let config = ViewGenerator.ViewConfig { .. }
+            let builtPlan = ViewGenerator.buildPlan' schema config
+
+            builtPlan `shouldBe`
+                [ EnsureDirectory {directory = "Web/View/Pages"},CreateFile {filePath = "Web/View/Pages/Edit.hs", fileContent = "module Web.View.Pages.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { page :: Page }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit Page</h1>\n        {renderForm page}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                , breadcrumbText \"Edit Page\"\n                ]\n\nrenderForm :: Page -> Html\nrenderForm page = formFor page [hsx|\n    \n    {submitButton}\n\n|]"},AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.Edit"}
+                ]
+
+
+        it "should build a view with name \"Edit\"" do
+            let rawViewName = "Edit"
+            let viewName = tableNameToViewName rawViewName
+            let rawControllerName = "Pages"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let paginationEnabled = False
+            let config = ViewGenerator.ViewConfig { .. }
+            let builtPlan = ViewGenerator.buildPlan' schema config
+
+            builtPlan `shouldBe`
+                [ EnsureDirectory {directory = "Web/View/Pages"},CreateFile {filePath = "Web/View/Pages/Edit.hs", fileContent = "module Web.View.Pages.Edit where\nimport Web.View.Prelude\n\ndata EditView = EditView { page :: Page }\n\ninstance View EditView where\n    html EditView { .. } = [hsx|\n        {breadcrumb}\n        <h1>Edit Page</h1>\n        {renderForm page}\n    |]\n        where\n            breadcrumb = renderBreadcrumb\n                [ breadcrumbLink \"Pages\" PagesAction\n                , breadcrumbText \"Edit Page\"\n                ]\n\nrenderForm :: Page -> Html\nrenderForm page = formFor page [hsx|\n    \n    {submitButton}\n\n|]"},AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.Edit"}
+                ]
+
+
+        it "should build a view with name \"Test\"" do
+            let rawViewName = "Test"
+            let viewName = tableNameToViewName rawViewName
+            let rawControllerName = "Pages"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let paginationEnabled = False
+            let config = ViewGenerator.ViewConfig { .. }
+            let builtPlan = ViewGenerator.buildPlan' schema config
+
+            builtPlan `shouldBe`
+                [ EnsureDirectory {directory = "Web/View/Pages"},CreateFile {filePath = "Web/View/Pages/Test.hs", fileContent = "module Web.View.Pages.Test where\nimport Web.View.Prelude\ndata TestView = TestView\n\ninstance View TestView where\n    html TestView { .. } = [hsx|\n        {breadcrumb}\n        <h1>TestView</h1>\n        |]\n            where\n                breadcrumb = renderBreadcrumb\n                                [ breadcrumbLink \"Tests\" PagesAction\n                                , breadcrumbText \"TestView\"\n                                ]"},AddImport {filePath = "Web/Controller/Pages.hs", fileContent = "import Web.View.Pages.Test"}
+                ]
+
+        it "should generate type-aware form fields for a rich schema" do
+            let richSchema = [
+                        StatementCreateTable (table "projects") {
+                            columns = [
+                                (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                                , (col "title" PText) { notNull = True }
+                                , (col "is_active" PBoolean) { notNull = True }
+                                , (col "budget" PInt) { notNull = False }
+                                , (col "started_on" PDate) { notNull = False }
+                                , (col "user_id" PUUID) { notNull = True }
+                            ]
+                            , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                            , constraints = [
+                                ForeignKeyConstraint { name = Nothing, columnName = "user_id", referenceTable = "users", referenceColumn = Nothing, onDelete = Nothing }
+                              ]
+                        }
+                    ]
+            let viewName = "NewView"
+            let rawControllerName = "projects"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let paginationEnabled = False
+            let config = ViewGenerator.ViewConfig { .. }
+            let builtPlan = ViewGenerator.buildPlan' richSchema config
+            let (EnsureDirectory {}):((CreateFile { fileContent = newViewContent }):_) = builtPlan
+            -- Type-aware form fields
+            (cs newViewContent :: String) `shouldContain` "{(textField #title)}"
+            (cs newViewContent :: String) `shouldContain` "{(checkboxField #isActive)}"
+            (cs newViewContent :: String) `shouldContain` "{(numberField #budget)}"
+            (cs newViewContent :: String) `shouldContain` "{(dateField #startedOn)}"
+            -- FK field should have a TODO comment
+            (cs newViewContent :: String) `shouldContain` "{- userId needs to be a selectField -}"
+
+        it "should generate dl-based show view for a rich schema" do
+            let richSchema = [
+                        StatementCreateTable (table "projects") {
+                            columns = [
+                                (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                                , (col "title" PText) { notNull = True }
+                                , (col "is_active" PBoolean) { notNull = True }
+                            ]
+                            , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        }
+                    ]
+            let viewName = "ShowView"
+            let rawControllerName = "projects"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let paginationEnabled = False
+            let config = ViewGenerator.ViewConfig { .. }
+            let builtPlan = ViewGenerator.buildPlan' richSchema config
+            let (EnsureDirectory {}):((CreateFile { fileContent = showViewContent }):_) = builtPlan
+            (cs showViewContent :: String) `shouldContain` "<dl>"
+            (cs showViewContent :: String) `shouldContain` "<dt>Title</dt><dd>{project.title}</dd>"
+            (cs showViewContent :: String) `shouldContain` "<dt>Is Active</dt><dd>{project.isActive}</dd>"
+
+        it "should generate column-based index view for a rich schema" do
+            let richSchema = [
+                        StatementCreateTable (table "projects") {
+                            columns = [
+                                (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                                , (col "title" PText) { notNull = True }
+                                , (col "is_active" PBoolean) { notNull = True }
+                            ]
+                            , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        }
+                    ]
+            let viewName = "IndexView"
+            let rawControllerName = "projects"
+            let controllerName = tableNameToControllerName rawControllerName
+            let modelName = tableNameToModelName rawControllerName
+            let applicationName = "Web"
+            let paginationEnabled = False
+            let config = ViewGenerator.ViewConfig { .. }
+            let builtPlan = ViewGenerator.buildPlan' richSchema config
+            let (EnsureDirectory {}):((CreateFile { fileContent = indexViewContent }):_) = builtPlan
+            (cs indexViewContent :: String) `shouldContain` "<th>Title</th>"
+            (cs indexViewContent :: String) `shouldContain` "<th>Is Active</th>"
+            (cs indexViewContent :: String) `shouldContain` "<td>{project.title}</td>"
+            (cs indexViewContent :: String) `shouldContain` "<td>{project.isActive}</td>"
diff --git a/Test/IDE/SchemaDesigner/CompilerSpec.hs b/Test/IDE/SchemaDesigner/CompilerSpec.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/SchemaDesigner/CompilerSpec.hs
@@ -0,0 +1,945 @@
+{-|
+Module: Test.IDE.SchemaDesigner.CompilerSpec
+Copyright: (c) digitally induced GmbH, 2020
+-}
+module Test.IDE.SchemaDesigner.CompilerSpec where
+
+import Test.Hspec
+import IHP.Prelude
+import IHP.Postgres.Compiler (compileSql)
+import IHP.Postgres.Types
+import Test.IDE.SchemaDesigner.ParserSpec (parseSql)
+
+tests = do
+    describe "The Schema.sql Compiler" do
+        it "should compile an empty CREATE TABLE statement" do
+            compileSql [StatementCreateTable (table "users")] `shouldBe` "CREATE TABLE users (\n\n);\n"
+
+        it "should compile a CREATE EXTENSION for the UUID extension" do
+            compileSql [CreateExtension { name = "uuid-ossp", ifNotExists = True }] `shouldBe` "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\n"
+
+        it "should compile a line comment" do
+            compileSql [Comment { content = " Comment value" }] `shouldBe` "-- Comment value\n"
+
+        it "should compile a empty line comments" do
+            compileSql [Comment { content = "" }, Comment { content = "" }] `shouldBe` "--\n--\n"
+
+        it "should compile a CREATE TABLE with columns" do
+            let sql = cs [plain|CREATE TABLE users (
+    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+    firstname TEXT NOT NULL,
+    lastname TEXT NOT NULL,
+    password_hash TEXT NOT NULL,
+    email TEXT NOT NULL,
+    company_id UUID NOT NULL,
+    picture_url TEXT,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
+);
+|]
+            let statement = StatementCreateTable (table "users")
+                    { columns = [
+                        (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                        , (col "firstname" PText) { notNull = True }
+                        , (col "lastname" PText) { notNull = True }
+                        , (col "password_hash" PText) { notNull = True }
+                        , (col "email" PText) { notNull = True }
+                        , (col "company_id" PUUID) { notNull = True }
+                        , col "picture_url" PText
+                        , (col "created_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
+                        ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE with quoted identifiers" do
+            compileSql [StatementCreateTable (table "quoted name")] `shouldBe` "CREATE TABLE \"quoted name\" (\n\n);\n"
+
+        it "should compile ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE CASCADE" do
+            let statement = AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just Cascade
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE;\n"
+
+        it "should compile ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE SET DEFAULT" do
+            let statement = AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just SetDefault
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE SET DEFAULT;\n"
+
+        it "should compile ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE SET NULL" do
+            let statement = AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just SetNull
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE SET NULL;\n"
+
+        it "should compile ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE RESTRICT" do
+            let statement = AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just Restrict
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE RESTRICT;\n"
+
+        it "should compile ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE NO ACTION" do
+            let statement = AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just NoAction
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE NO ACTION;\n"
+
+        it "should compile ALTER TABLE .. ADD FOREIGN KEY .. (without ON DELETE)" do
+            let statement = AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Nothing
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ;\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. CHECK .." do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression = NotEqExpression (VarExpression "title") (TextExpression "")
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (title <> '');\n"
+
+        it "should compile a complex ALTER TABLE .. ADD CONSTRAINT .. CHECK .." do
+            let statement = AddConstraint
+                    { tableName = "properties"
+                    , constraint = CheckConstraint
+                        { name = "foobar"
+                        , checkExpression = OrExpression
+                                (AndExpression
+                                    (AndExpression
+                                        (EqExpression (VarExpression "property_type") (TextExpression "haus_buy"))
+                                        (IsExpression (VarExpression "area_garden") (NotExpression (VarExpression "NULL")))
+                                    )
+                                    (IsExpression (VarExpression "rent_monthly") (VarExpression "NULL"))
+                                )
+
+                                (AndExpression
+                                    (AndExpression
+                                        (EqExpression (VarExpression "property_type") (TextExpression "haus_rent"))
+                                        (IsExpression (VarExpression "rent_monthly") (NotExpression (VarExpression "NULL")))
+                                    )
+                                    (IsExpression (VarExpression "price") (VarExpression "NULL"))
+                                )
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE properties ADD CONSTRAINT foobar CHECK ((property_type = 'haus_buy' AND area_garden IS NOT NULL AND rent_monthly IS NULL) OR (property_type = 'haus_rent' AND rent_monthly IS NOT NULL AND price IS NULL));\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. CHECK .. with a <" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression = LessThanExpression (CallExpression ("length") [VarExpression "title"]) (VarExpression "20")
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (length(title) < 20);\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. CHECK .. with a <=" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression = LessThanOrEqualToExpression (CallExpression ("length") [VarExpression "title"]) (VarExpression "20")
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (length(title) <= 20);\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. CHECK .. with a >" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression = GreaterThanExpression (CallExpression ("length") [VarExpression "title"]) (VarExpression "20")
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (length(title) > 20);\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. CHECK .. with a >=" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression = GreaterThanOrEqualToExpression (CallExpression ("length") [VarExpression "title"]) (VarExpression "20")
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (length(title) >= 20);\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .." do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (title WITH =, author WITH =);\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. USING BTREE" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Just Btree
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE USING BTREE (title WITH =, author WITH =);\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. USING GIST" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Just Gist
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE USING GIST (title WITH =, author WITH =);\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. USING GIN" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Just Gin
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE USING GIN (title WITH =, author WITH =);\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. WHERE .." do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Just $ EqExpression (VarExpression "title") (TextExpression "why")
+                        , indexType = Nothing
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (title WITH =, author WITH =) WHERE (title = 'why');\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. WHERE .. with various operators" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "i1", operator = "=" }
+                            , ExcludeConstraintElement { element = "i2", operator = "<>" }
+                            , ExcludeConstraintElement { element = "i3", operator = "!=" }
+                            , ExcludeConstraintElement { element = "i4", operator = "AND" }
+                            , ExcludeConstraintElement { element = "i5", operator = "OR" }
+                            ]
+                        , predicate = Just $ EqExpression (VarExpression "title") (TextExpression "why")
+                        , indexType = Nothing
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (i1 WITH =, i2 WITH <>, i3 WITH !=, i4 WITH AND, i5 WITH OR) WHERE (title = 'why');\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. DEFERRABLE" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Just True
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (title WITH =) DEFERRABLE;\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. NOT DEFERRABLE" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Just False
+                    , deferrableType = Nothing
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (title WITH =) NOT DEFERRABLE;\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. DEFERRABLE INITIALLY IMMEDIATE" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Just True
+                    , deferrableType = Just InitiallyImmediate
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (title WITH =) DEFERRABLE INITIALLY IMMEDIATE;\n"
+
+        it "should compile ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. DEFERRABLE INITIALLY DEFERRED" do
+            let statement = AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Just True
+                    , deferrableType = Just InitiallyDeferred
+                    }
+            compileSql [statement] `shouldBe` "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (title WITH =) DEFERRABLE INITIALLY DEFERRED;\n"
+
+        it "should compile a CREATE TABLE with text default value in columns" do
+            let sql = cs [plain|CREATE TABLE a (\n    content TEXT DEFAULT 'example text' NOT NULL\n);\n|]
+            let statement = StatementCreateTable (table "a")
+                    { columns = [
+                        (col "content" PText) { defaultValue = Just (TextExpression "example text"), notNull = True }
+                        ]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TYPE .. AS ENUM" do
+            let sql = cs [plain|CREATE TYPE colors AS ENUM ('yellow', 'red', 'blue');\n|]
+            let statement = CreateEnumType
+                    { name = "colors"
+                    , values = ["yellow", "red", "blue"]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE with (deprecated) NUMERIC, NUMERIC(x), NUMERIC (x,y), VARYING(n) columns" do
+            let sql = cs [plain|CREATE TABLE deprecated_variables (\n    a NUMERIC,\n    b NUMERIC(1),\n    c NUMERIC(1,2),\n    d CHARACTER VARYING(10)\n);\n|]
+            let statement = StatementCreateTable (table "deprecated_variables")
+                    { columns =
+                        [ col "a" (PNumeric Nothing Nothing)
+                        , col "b" (PNumeric (Just 1) Nothing)
+                        , col "c" (PNumeric (Just 1) (Just 2))
+                        , col "d" (PVaryingN (Just 10))
+                        ]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE statement with a multi-column UNIQUE (a, b) constraint" do
+            let sql = cs [plain|CREATE TABLE user_followers (\n    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,\n    user_id UUID NOT NULL,\n    follower_id UUID NOT NULL,\n    UNIQUE(user_id, follower_id)\n);\n|]
+            let statement = StatementCreateTable (table "user_followers")
+                    { columns =
+                        [ (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                        , (col "user_id" PUUID) { notNull = True }
+                        , (col "follower_id" PUUID) { notNull = True }
+                        ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    , constraints = [ UniqueConstraint { name = Nothing, columnNames = [ "user_id", "follower_id" ] } ]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE statement with a serial id" do
+            let sql = cs [plain|CREATE TABLE orders (\n    id SERIAL PRIMARY KEY NOT NULL\n);\n|]
+            let statement = StatementCreateTable (table "orders")
+                    { columns = [ (col "id" PSerial) { notNull = True } ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE statement with a bigserial id" do
+            let sql = cs [plain|CREATE TABLE orders (\n    id BIGSERIAL PRIMARY KEY NOT NULL\n);\n|]
+            let statement = StatementCreateTable (table "orders")
+                    { columns = [ (col "id" PBigserial) { notNull = True } ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE statement with a composite primary key" do
+            let sql = cs [plain|CREATE TABLE "orderTrucks" (\n    order_id BIGSERIAL NOT NULL,\n    truck_id BIGSERIAL NOT NULL,\n    PRIMARY KEY(order_id, truck_id)\n);\n|]
+            let statement = StatementCreateTable (table "orderTrucks")
+                    { columns =
+                        [ (col "order_id" PBigserial) { notNull = True }
+                        , (col "truck_id" PBigserial) { notNull = True }
+                        ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["order_id", "truck_id"]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE statement with an array column" do
+            let sql = cs [plain|CREATE TABLE array_tests (\n    pay_by_quarter INT[]\n);\n|]
+            let statement = StatementCreateTable (table "array_tests")
+                    { columns = [ col "pay_by_quarter" (PArray PInt) ]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE statement with an point column" do
+            let sql = cs [plain|CREATE TABLE point_tests (\n    pos POINT\n);\n|]
+            let statement = StatementCreateTable (table "point_tests")
+                    { columns = [ col "pos" PPoint ]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TABLE statement with an polygon column" do
+            let sql = cs [plain|CREATE TABLE polygon_tests (\n    poly POLYGON\n);\n|]
+            let statement = StatementCreateTable (table "polygon_tests")
+                    { columns = [ col "poly" PPolygon ]
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE INDEX statement" do
+            let sql = cs [plain|CREATE INDEX users_index ON users (user_name);\n|]
+            let statement = CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+            compileSql [statement] `shouldBe` sql
+        
+        it "should escape an index name inside a 'CREATE INDEX' statement" do
+            let sql = cs [plain|CREATE INDEX "Some Index" ON "Some Table" ("Some Col");\n|]
+            let statement = CreateIndex
+                    { indexName = "Some Index"
+                    , unique = False
+                    , tableName = "Some Table"
+                    , columns = [indexCol (VarExpression "Some Col")]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a 'CREATE INDEX .. ON .. USING GIN' statement" do
+            let sql = cs [plain|CREATE INDEX users_index ON users USING GIN (user_name);\n|]
+            let statement = CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Just Gin
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a 'CREATE INDEX .. ON .. USING BTREE' statement" do
+            let sql = cs [plain|CREATE INDEX users_index ON users USING BTREE (user_name);\n|]
+            let statement = CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Just Btree
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a 'CREATE INDEX .. ON .. USING GIST' statement" do
+            let sql = cs [plain|CREATE INDEX users_index ON users USING GIST (user_name);\n|]
+            let statement = CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Just Gist
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE INDEX statement with multiple columns" do
+            let sql = cs [plain|CREATE INDEX users_index ON users (user_name, project_id);\n|]
+            let statement = CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns =
+                        [ indexCol (VarExpression "user_name")
+                        , indexCol (VarExpression "project_id")
+                        ]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE INDEX statement with a LOWER call" do
+            let sql = cs [plain|CREATE INDEX users_email_index ON users (LOWER(email));\n|]
+            let statement = CreateIndex
+                    { indexName = "users_email_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (CallExpression "LOWER" [VarExpression "email"])]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE UNIQUE INDEX statement" do
+            let sql = cs [plain|CREATE UNIQUE INDEX users_index ON users (user_name);\n|]
+            let statement = CreateIndex
+                    { indexName = "users_index"
+                    , unique = True
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE INDEX with column order ASC NULLS FIRST statement" do
+            let sql = cs [plain|CREATE UNIQUE INDEX users_index ON users (user_name ASC NULLS FIRST);\n|]
+            let statement = CreateIndex
+                    { indexName = "users_index"
+                    , unique = True
+                    , tableName = "users"
+                    , columns = [IndexColumn { column = VarExpression "user_name", columnOrder = [Asc, NullsFirst]}]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE INDEX with column order DESC NULLS LAST statement" do
+            let sql = cs [plain|CREATE UNIQUE INDEX users_index ON users (user_name DESC NULLS LAST);\n|]
+            let statement = CreateIndex
+                    { indexName = "users_index"
+                    , unique = True
+                    , tableName = "users"
+                    , columns = [IndexColumn { column = VarExpression "user_name", columnOrder = [Desc, NullsLast]}]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE OR REPLACE FUNCTION ..() RETURNS TRIGGER .." do
+            let sql = cs [plain|CREATE OR REPLACE FUNCTION notify_did_insert_webrtc_connection() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; $$ language plpgsql;\n|]
+            let statement = (function "notify_did_insert_webrtc_connection")
+                    { functionBody = " BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; "
+                    , orReplace = True
+                    }
+
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE OR REPLACE FUNCTION ..() RETURNS EVENT_TRIGGER .." do
+            let sql = cs [plain|CREATE OR REPLACE FUNCTION a() RETURNS EVENT_TRIGGER AS $$$$ language plpgsql;\n|]
+            let statement = (function "a")
+                    { orReplace = True
+                    , returns = PEventTrigger
+                    }
+
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE FUNCTION ..() RETURNS TRIGGER .." do
+            let sql = cs [plain|CREATE FUNCTION notify_did_insert_webrtc_connection() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; $$ language plpgsql;\n|]
+            let statement = (function "notify_did_insert_webrtc_connection")
+                    { functionBody = " BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; "
+                    }
+
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE FUNCTION with parameters ..() RETURNS TRIGGER .." do
+            let sql = cs [plain|CREATE FUNCTION notify_did_insert_webrtc_connection(param1 TEXT, param2 INT) RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; $$ language plpgsql;\n|]
+            let statement = (function "notify_did_insert_webrtc_connection")
+                    { functionArguments = [("param1", PText), ("param2", PInt)]
+                    , functionBody = " BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; "
+                    }
+
+            compileSql [statement] `shouldBe` sql
+
+
+        it "should compile a CREATE FUNCTION with SECURITY DEFINER" do
+            let sql = cs [plain|CREATE FUNCTION my_func() RETURNS TRIGGER SECURITY DEFINER AS $$ BEGIN RETURN NEW; END; $$ language plpgsql;\n|]
+            let statement = (function "my_func")
+                    { functionBody = " BEGIN RETURN NEW; END; "
+                    , securityDefiner = True
+                    }
+
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a CREATE TRIGGER .." do
+            let sql = cs [plain|CREATE TRIGGER t AFTER INSERT ON x FOR EACH ROW EXECUTE PROCEDURE y();\n|]
+            let statement = UnknownStatement { raw = "CREATE TRIGGER t AFTER INSERT ON x FOR EACH ROW EXECUTE PROCEDURE y()"  }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a decimal default value with a type-cast" do
+            let sql = "CREATE TABLE a (\n    electricity_unit_price DOUBLE PRECISION DEFAULT 0.17::DOUBLE PRECISION NOT NULL\n);\n"
+            let statement = StatementCreateTable (table "a") { columns = [(col "electricity_unit_price" PDouble) { defaultValue = Just (TypeCastExpression (DoubleExpression 0.17) PDouble), notNull = True }] }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a integer default value" do
+            let sql = "CREATE TABLE a (\n    electricity_unit_price INT DEFAULT 0 NOT NULL\n);\n"
+            let statement = StatementCreateTable (table "a") { columns = [(col "electricity_unit_price" PInt) { defaultValue = Just (IntExpression 0), notNull = True }] }
+            compileSql [statement] `shouldBe` sql
+
+        it "should compile a partial index" do
+            let sql = cs [plain|CREATE UNIQUE INDEX unique_source_id ON listings (source, source_id) WHERE source IS NOT NULL AND source_id IS NOT NULL;\n|]
+            let index = CreateIndex
+                    { indexName = "unique_source_id"
+                    , unique = True
+                    , tableName = "listings"
+                    , columns =
+                        [ indexCol (VarExpression "source")
+                        , indexCol (VarExpression "source_id")
+                        ]
+                    , whereClause = Just (
+                        AndExpression
+                            (IsExpression (VarExpression "source") (NotExpression (VarExpression "NULL")))
+                            (IsExpression (VarExpression "source_id") (NotExpression (VarExpression "NULL"))))
+                    , indexType = Nothing
+                    }
+            compileSql [index] `shouldBe` sql
+
+        it "should compile 'ENABLE ROW LEVEL SECURITY' statements" do
+            let sql = "ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;\n"
+            let statements = [EnableRowLevelSecurity { tableName = "tasks" }]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ENABLE ROW LEVEL SECURITY' statements with spaces in the table name" do
+            let sql = "ALTER TABLE \"users (old)\" ENABLE ROW LEVEL SECURITY;\n"
+            let statements = [EnableRowLevelSecurity { tableName = "users (old)" }]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'CREATE POLICY' statements" do
+            let sql = "CREATE POLICY \"Users can manage their tasks\" ON tasks USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());\n"
+            let p = (policy "Users can manage their tasks" "tasks")
+                    { using = Just (
+                        EqExpression
+                            (VarExpression "user_id")
+                            (CallExpression "ihp_user_id" [])
+                        )
+                    , check = Just (
+                        EqExpression
+                            (VarExpression "user_id")
+                            (CallExpression "ihp_user_id" [])
+                        )
+                    }
+            compileSql [p] `shouldBe` sql
+
+        it "should compile 'CREATE POLICY' statements with a 'ihp_user_id() IS NOT NULL' expression" do
+            -- https://github.com/digitallyinduced/ihp/issues/1412
+            let sql = "CREATE POLICY \"Users can manage tasks if logged in\" ON tasks USING (ihp_user_id() IS NOT NULL) WITH CHECK (ihp_user_id() IS NOT NULL);\n"
+            let p = (policy "Users can manage tasks if logged in" "tasks")
+                    { using = Just (
+                        IsExpression
+                            (CallExpression "ihp_user_id" [])
+                            (NotExpression (VarExpression "NULL"))
+                        )
+                    , check = Just (
+                        IsExpression
+                            (CallExpression "ihp_user_id" [])
+                            (NotExpression (VarExpression "NULL"))
+                        )
+                    }
+            compileSql [p] `shouldBe` sql
+
+        it "should compile 'CREATE POLICY .. FOR SELECT' statements" do
+            let sql = "CREATE POLICY \"Messages are public\" ON messages FOR SELECT USING (true);\n"
+            let p = CreatePolicy
+                    { name = "Messages are public"
+                    , action = Just PolicyForSelect
+                    , tableName = "messages"
+                    , using = Just (VarExpression "true")
+                    , check = Nothing
+                    }
+            compileSql [p] `shouldBe` sql
+
+        it "should use parentheses where needed" do
+            -- https://github.com/digitallyinduced/ihp/issues/1087
+            let inputSql = cs [plain|ALTER TABLE listings ADD CONSTRAINT source CHECK ((NOT (user_id IS NOT NULL AND agent_id IS NOT NULL)) AND (user_id IS NOT NULL OR agent_id IS NOT NULL));\n|]
+            compileSql [parseSql inputSql] `shouldBe` inputSql
+
+        it "should compile 'ALTER TABLE .. DROP COLUMN ..' statements" do
+            let sql = "ALTER TABLE tasks DROP COLUMN description;\n"
+            let statements = [ DropColumn { tableName = "tasks", columnName = "description" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'DROP TABLE ..' statements" do
+            let sql = "DROP TABLE tasks;\n"
+            let statements = [ DropTable { tableName = "tasks" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'CREATE SEQUENCE ..' statements" do
+            let sql = "CREATE SEQUENCE a;\n"
+            let statements = [ CreateSequence { name = "a" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. RENAME COLUMN .. TO ..' statements" do
+            let sql = "ALTER TABLE users RENAME COLUMN name TO full_name;\n"
+            let statements = [ RenameColumn { tableName = "users", from = "name", to = "full_name" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. ADD UNIQUE (..);' statements" do
+            let sql = "ALTER TABLE users ADD UNIQUE (full_name);\n"
+            let statements = [ AddConstraint { tableName = "users", constraint = UniqueConstraint { name = Nothing, columnNames = ["full_name"] }, deferrable = Nothing, deferrableType = Nothing } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. DROP CONSTRAINT ..;' statements" do
+            let sql = "ALTER TABLE users DROP CONSTRAINT users_full_name_key;\n"
+            let statements = [ DropConstraint { tableName = "users", constraintName = "users_full_name_key" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'DROP TYPE ..;' statements" do
+            let sql = "DROP TYPE colors;\n"
+            let statements = [ DropEnumType { name = "colors" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'DROP INDEX ..;' statements" do
+            let sql = "DROP INDEX a;\n"
+            let statements = [ DropIndex { indexName = "a" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. ALTER COLUMN .. DROP NOT NULL;' statements" do
+            let sql = "ALTER TABLE users ALTER COLUMN email DROP NOT NULL;\n"
+            let statements = [ DropNotNull { tableName = "users", columnName = "email" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. ALTER COLUMN .. SET NOT NULL;' statements" do
+            let sql = "ALTER TABLE users ALTER COLUMN email SET NOT NULL;\n"
+            let statements = [ SetNotNull { tableName = "users", columnName = "email" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. ALTER COLUMN .. SET DEFAULT ..;' statements" do
+            let sql = "ALTER TABLE users ALTER COLUMN email SET DEFAULT null;\n"
+            let statements = [ SetDefaultValue { tableName = "users", columnName = "email", value = VarExpression "null" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. ALTER COLUMN .. DROP DEFAULT;' statements" do
+            let sql = "ALTER TABLE users ALTER COLUMN email DROP DEFAULT;\n"
+            let statements = [ DropDefaultValue { tableName = "users", columnName = "email" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. RENAME TO ..;' statements" do
+            let sql = "ALTER TABLE profiles RENAME TO users;\n"
+            let statements = [ RenameTable { from = "profiles", to = "users" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'DROP POLICY .. ON ..;' statements" do
+            let sql = "DROP POLICY \"Users can manage their todos\" ON todos;\n"
+            let statements = [ DropPolicy { tableName = "todos", policyName = "Users can manage their todos" } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'CREATE EXTENSION IF NOT EXISTS;' statements with an unqualified name" do
+            let sql = "CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;\n"
+            let statements = [ CreateExtension { name = "fuzzystrmatch", ifNotExists = True } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'CREATE POLICY ..;' statements with an EXISTS condition" do
+            let sql = cs [plain|CREATE POLICY "Users can manage their project's migrations" ON migrations USING (EXISTS (SELECT 1 FROM public.projects WHERE projects.id = migrations.project_id)) WITH CHECK (EXISTS (SELECT 1 FROM public.projects WHERE projects.id = migrations.project_id));\n|]
+            let statements =
+                    [ (policy "Users can manage their project's migrations" "migrations")
+                        { using = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = DotExpression (VarExpression "public") "projects", alias = Nothing, whereClause = EqExpression (DotExpression (VarExpression "projects") "id") (DotExpression (VarExpression "migrations") "project_id")})))
+                        , check = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = DotExpression (VarExpression "public") "projects", alias = Nothing, whereClause = EqExpression (DotExpression (VarExpression "projects") "id") (DotExpression (VarExpression "migrations") "project_id")})))
+                        }
+                    ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TYPE .. ADD VALUE ..;' statements" do
+            let sql = "ALTER TYPE colors ADD VALUE 'blue';\n"
+            let statements = [ AddValueToEnumType { enumName = "colors", newValue = "blue", ifNotExists = False } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'CREATE TRIGGER .. AFTER INSERT ON .. FOR EACH ROW EXECUTE ..;' statements" do
+            let sql = "CREATE TRIGGER call_test_function_for_new_users AFTER INSERT ON users FOR EACH ROW EXECUTE FUNCTION call_test_function('hello');\n"
+            let statements = [ CreateTrigger
+                    { name = "call_test_function_for_new_users"
+                    , eventWhen = After
+                    , event = [TriggerOnInsert]
+                    , tableName = "users"
+                    , for = ForEachRow
+                    , whenCondition = Nothing
+                    , functionName = "call_test_function"
+                    , arguments = [TextExpression "hello"]
+                    } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'CREATE TRIGGER .. AFTER INSERT OR UPDATE ON ..' statements" do
+            let sql = "CREATE TRIGGER my_trigger AFTER INSERT OR UPDATE ON posts FOR EACH ROW EXECUTE FUNCTION my_function();\n"
+            let statements = [ CreateTrigger
+                    { name = "my_trigger"
+                    , eventWhen = After
+                    , event = [TriggerOnInsert, TriggerOnUpdate]
+                    , tableName = "posts"
+                    , for = ForEachRow
+                    , whenCondition = Nothing
+                    , functionName = "my_function"
+                    , arguments = []
+                    } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'CREATE EVENT TRIGGER ..' statements" do
+            let sql = "CREATE EVENT TRIGGER trigger_update_schema ON ddl_command_end WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE', 'DROP TABLE') EXECUTE FUNCTION update_tables_and_columns();\n"
+            let statements = [ CreateEventTrigger
+                    { name = "trigger_update_schema"
+                    , eventOn = "ddl_command_end"
+                    , whenCondition =  Just (InExpression (VarExpression "TAG") (InArrayExpression [TextExpression "CREATE TABLE", TextExpression "ALTER TABLE", TextExpression "DROP TABLE"]))
+                    , functionName = "update_tables_and_columns"
+                    , arguments = []
+                    } ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'BEGIN;' statements" do
+            let sql = "BEGIN;\n"
+            let statements = [ Begin ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'COMMIT;' statements" do
+            let sql = "COMMIT;\n"
+            let statements = [ Commit ]
+            compileSql statements `shouldBe` sql
+
+        it "should compile 'ALTER TABLE .. ALTER COLUMN .. DROP NOT NULL;' statements" do
+            let sql = "COMMIT;\n"
+            let statements = [ Commit ]
+            compileSql statements `shouldBe` sql
+        it "should compile 'GENERATED' columns" do
+            let sql = [trimming|
+                CREATE TABLE products (
+                    ts TSVECTOR GENERATED ALWAYS AS (setweight(to_tsvector('english', sku), ('A'::"char")) || setweight(to_tsvector('english', name), 'B') || setweight(to_tsvector('english', description), 'C')) STORED
+                );
+            |] <> "\n"
+            let statements = [
+                        StatementCreateTable (table "products")
+                            { columns = [
+                                (col "ts" PTSVector) { generator = Just $ ColumnGenerator
+                                                { generate =
+                                                    ConcatenationExpression
+                                                        (ConcatenationExpression
+                                                            (CallExpression "setweight" [CallExpression "to_tsvector" [TextExpression "english", VarExpression "sku"], TypeCastExpression (TextExpression "A") PSingleChar])
+                                                            (CallExpression "setweight" [CallExpression "to_tsvector" [TextExpression "english", VarExpression "name"], TextExpression "B"])
+                                                        )
+                                                        (CallExpression "setweight" [CallExpression "to_tsvector" [TextExpression "english", VarExpression "description"], TextExpression "C"]), stored = True }
+                                    }
+                                ]
+                            }
+                        ]
+            compileSql statements `shouldBe` sql
+        it "should compile 'DROP FUNCTION ..;' statements" do
+            let sql = "DROP FUNCTION my_function;\n"
+            let statements = [ DropFunction { functionName = "my_function" } ]
+            compileSql statements `shouldBe` sql
+        it "should compile 'CREATE UNLOGGED TABLE' statements" do
+            let sql = [trimming|
+                CREATE UNLOGGED TABLE pg_large_notifications (
+                
+                );
+            |] <> "\n"
+            let statements = [
+                        StatementCreateTable (table "pg_large_notifications")
+                            { unlogged = True, inherits = Nothing
+                            }
+                        ]
+            compileSql statements `shouldBe` sql
+
+        it "should escape policy names with different casing" do
+            let sql = [trimming|
+                CREATE POLICY "Public" ON plans USING (true) WITH CHECK (false);
+            |] <> "\n"
+            let statements = [
+                        (policy "Public" "plans")
+                            { using = Just (VarExpression "true")
+                            , check = Just (VarExpression "false")
+                            }
+                        ]
+            compileSql statements `shouldBe` sql
diff --git a/Test/IDE/SchemaDesigner/Controller/EnumValuesSpec.hs b/Test/IDE/SchemaDesigner/Controller/EnumValuesSpec.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/SchemaDesigner/Controller/EnumValuesSpec.hs
@@ -0,0 +1,22 @@
+module Test.IDE.SchemaDesigner.Controller.EnumValuesSpec where
+
+import Test.Hspec
+import IHP.Prelude
+import IHP.IDE.SchemaDesigner.Controller.EnumValues
+import IHP.Postgres.Types
+
+tests :: SpecWith ()
+tests = do
+    describe "IHP.IDE.SchemaDesigner.Controller.EnumValues" do
+        describe "getAllEnumValues" do
+            it "should return a list of all enum values" do
+                getAllEnumValues [] `shouldBe` []
+                getAllEnumValues [ CreateExtension { name ="a", ifNotExists = True } ] `shouldBe` []
+                getAllEnumValues [ CreateEnumType { name = "first_enum", values=["a", "b", "c"] }] `shouldBe` ["a", "b", "c"]
+                getAllEnumValues
+                    [ CreateEnumType {name = "first_enum", values = ["a", "b"]}
+                    , CreateExtension {name = "extension", ifNotExists = True}
+                    , CreateEnumType {name = "second_enum", values = ["c"]}
+                    , CreateEnumType {name = "third_enum", values = []}
+                    ]
+                    `shouldBe` ["a","b","c"]
diff --git a/Test/IDE/SchemaDesigner/Controller/HelperSpec.hs b/Test/IDE/SchemaDesigner/Controller/HelperSpec.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/SchemaDesigner/Controller/HelperSpec.hs
@@ -0,0 +1,24 @@
+module Test.IDE.SchemaDesigner.Controller.HelperSpec where
+
+import Test.Hspec
+import IHP.Prelude
+import IHP.IDE.SchemaDesigner.Controller.Helper
+import IHP.Postgres.Types
+
+tests :: SpecWith ()
+tests = do
+    describe "IHP.IDE.SchemaDesigner.Controller.Helper" do
+        describe "getAllObjectNames" do
+            it "should return a list of all names of tables and enum types" do
+                getAllObjectNames [] `shouldBe` []
+                getAllObjectNames [ CreateExtension { name ="a", ifNotExists = True } ] `shouldBe` []
+                getAllObjectNames [ CreateEnumType { name = "first_enum", values=["a", "b", "c"] }] `shouldBe` ["first_enum"]
+                getAllObjectNames [ StatementCreateTable (table "table_name") ]
+                    `shouldBe` ["table_name"]
+                getAllObjectNames
+                    [ CreateEnumType {name = "first_enum", values = ["a", "b"]}
+                    , CreateExtension {name = "extension", ifNotExists = True}
+                    , StatementCreateTable (table "table_name")
+                    , CreateEnumType {name = "second_enum", values = []}
+                    ]
+                    `shouldBe` ["first_enum","table_name","second_enum"]
diff --git a/Test/IDE/SchemaDesigner/Controller/ValidationSpec.hs b/Test/IDE/SchemaDesigner/Controller/ValidationSpec.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/SchemaDesigner/Controller/ValidationSpec.hs
@@ -0,0 +1,33 @@
+module Test.IDE.SchemaDesigner.Controller.ValidationSpec where
+
+import Test.Hspec
+import IHP.Prelude
+
+import IHP.IDE.SchemaDesigner.Controller.Validation
+import IHP.ValidationSupport
+
+tests :: SpecWith ()
+tests = do
+    describe "IHP.IDE.SchemaDesigner.Controller.Validation" do
+        describe "the isUniqueInList validator" do
+            it "should handle cases wihout old value" do
+                isUniqueInList [] Nothing "hello" `shouldBe` Success
+                isUniqueInList ["hi", "bye"] Nothing "hello" `shouldBe` Success
+                isUniqueInList ["hi"] Nothing "hi" `shouldSatisfy` isFailure
+            it "should handle cases with old value" do
+                isUniqueInList ["bye"] (Just "bye") "hello" `shouldBe` Success
+                isUniqueInList ["bye", "hi"] (Just "hi") "hi" `shouldBe` Success
+                isUniqueInList ["bye", "hi"] (Just "bye") "hi" `shouldSatisfy` isFailure
+
+        describe "the validateNameInSchema validator" do
+            it "should reject an empty name" do
+                validateNameInSchema "" [] Nothing "" `shouldSatisfy` isFailure
+            it "should reject a reserved keyword" do
+                validateNameInSchema "" [] Nothing "bigint" `shouldSatisfy` isFailure
+                validateNameInSchema "" [] Nothing "where" `shouldSatisfy` isFailure
+            it "should reject a new name already in use" do
+                validateNameInSchema "" ["used"] Nothing "used" `shouldSatisfy` isFailure
+            it "should allow a name already in used if it is the old name" do
+                validateNameInSchema "" ["used"] (Just "used") "used" `shouldBe` Success
+            it "should allow legal names not already in use" do
+                validateNameInSchema "" [] Nothing "new_name" `shouldBe` Success
diff --git a/Test/IDE/SchemaDesigner/ParserSpec.hs b/Test/IDE/SchemaDesigner/ParserSpec.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/SchemaDesigner/ParserSpec.hs
@@ -0,0 +1,1066 @@
+{-|
+Module: Test.IDE.SchemaDesigner.ParserSpec
+Copyright: (c) digitally induced GmbH, 2020
+-}
+module Test.IDE.SchemaDesigner.ParserSpec where
+
+import Test.Hspec
+import IHP.Prelude
+import qualified IHP.Postgres.Parser as Parser
+import IHP.Postgres.Types
+import qualified Text.Megaparsec as Megaparsec
+import GHC.IO (evaluate)
+
+tests = do
+    describe "The Schema.sql Parser" do
+        it "should parse an empty CREATE TABLE statement" do
+            parseSql "CREATE TABLE users ();"  `shouldBe` StatementCreateTable (table "users")
+
+        it "should parse an CREATE EXTENSION for the UUID extension" do
+            parseSql "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" `shouldBe` CreateExtension { name = "uuid-ossp", ifNotExists = True }
+
+        it "should parse an CREATE EXTENSION with schema suffix" do
+            parseSql "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\" WITH SCHEMA public;" `shouldBe` CreateExtension { name = "uuid-ossp", ifNotExists = True }
+
+        it "should parse an CREATE EXTENSION without quotes" do
+            parseSql "CREATE EXTENSION IF NOT EXISTS fuzzystrmatch WITH SCHEMA public;" `shouldBe` CreateExtension { name = "fuzzystrmatch", ifNotExists = True }
+
+        it "should parse a line comment" do
+            parseSql "-- Comment value" `shouldBe` Comment { content = " Comment value" }
+
+        it "should parse an empty comment" do
+            parseSqlStatements "--\n--" `shouldBe` [ Comment { content = "" }, Comment { content = "" } ]
+
+        it "should parse a CREATE TABLE with columns" do
+            let sql = cs [plain|CREATE TABLE users (
+                    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                    firstname TEXT NOT NULL,
+                    lastname TEXT NOT NULL,
+                    password_hash TEXT NOT NULL,
+                    email TEXT NOT NULL,
+                    company_id UUID NOT NULL,
+                    picture_url TEXT,
+                    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
+                ); |]
+            parseSql sql `shouldBe` StatementCreateTable (table "users")
+                    { columns = [
+                        (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                        , (col "firstname" PText) { notNull = True }
+                        , (col "lastname" PText) { notNull = True }
+                        , (col "password_hash" PText) { notNull = True }
+                        , (col "email" PText) { notNull = True }
+                        , (col "company_id" PUUID) { notNull = True }
+                        , col "picture_url" PText
+                        , (col "created_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
+                        ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    , constraints = []
+                    , unlogged = False
+                    , inherits = Nothing
+                    }
+
+        it "should parse a CREATE TABLE with a generated column" do
+            let sql = cs [plain|
+                CREATE TABLE products (
+                        ts tsvector GENERATED ALWAYS AS (setweight(to_tsvector('english', sku), 'A') || setweight(to_tsvector('english', name), 'B') || setweight(to_tsvector('english', description), 'C')) STORED
+                );
+            |]
+            parseSql sql `shouldBe` StatementCreateTable (table "products")
+                    { columns = [
+                        (col "ts" PTSVector)
+                            { generator = Just $ ColumnGenerator
+                                        { generate =
+                                            ConcatenationExpression
+                                                (ConcatenationExpression
+                                                    (CallExpression "setweight" [CallExpression "to_tsvector" [TextExpression "english",VarExpression "sku"],TextExpression "A"])
+                                                    (CallExpression "setweight" [CallExpression "to_tsvector" [TextExpression "english",VarExpression "name"],TextExpression "B"])
+                                                )
+                                                (CallExpression "setweight" [CallExpression "to_tsvector" [TextExpression "english",VarExpression "description"],TextExpression "C"])
+                                        , stored = True
+                                        }
+                            }
+                        ]
+                    }
+
+        it "should parse a CREATE TABLE with quoted identifiers" do
+            parseSql "CREATE TABLE \"quoted name\" ();" `shouldBe` StatementCreateTable (table "quoted name")
+
+        it "should parse a CREATE TABLE with public schema prefix" do
+            parseSql "CREATE TABLE public.users ();" `shouldBe` StatementCreateTable (table "users")
+
+        it "should parse ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE CASCADE" do
+            parseSql "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE;" `shouldBe` AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just Cascade
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE SET DEFAULT" do
+            parseSql "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE SET DEFAULT;" `shouldBe` AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just SetDefault
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE SET NULL" do
+            parseSql "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE SET NULL;" `shouldBe` AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just SetNull
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE RESTRICT" do
+            parseSql "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE RESTRICT;" `shouldBe` AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just Restrict
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD FOREIGN KEY .. ON DELETE NO ACTION" do
+            parseSql "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE NO ACTION;" `shouldBe` AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Just NoAction
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD FOREIGN KEY .. (without ON DELETE)" do
+            parseSql "ALTER TABLE users ADD CONSTRAINT users_ref_company_id FOREIGN KEY (company_id) REFERENCES companies (id);" `shouldBe` AddConstraint
+                    { tableName = "users"
+                    , constraint = ForeignKeyConstraint
+                        { name = "users_ref_company_id"
+                        , columnName = "company_id"
+                        , referenceTable = "companies"
+                        , referenceColumn = "id"
+                        , onDelete = Nothing
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. CHECK .." do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (title <> '');" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression = NotEqExpression (VarExpression "title") (TextExpression "")
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse a complex ALTER TABLE .. ADD CONSTRAINT .. CHECK .." do
+            parseSql "ALTER TABLE properties ADD CONSTRAINT foobar CHECK ((property_type = 'haus_buy' AND area_garden IS NOT NULL AND rent_monthly IS NULL) OR (property_type = 'haus_rent' AND rent_monthly IS NOT NULL AND price IS NULL));" `shouldBe` AddConstraint
+                    { tableName = "properties"
+                    , constraint = CheckConstraint
+                        { name = "foobar"
+                        , checkExpression = OrExpression
+                                (AndExpression
+                                    (AndExpression
+                                        (EqExpression (VarExpression "property_type") (TextExpression "haus_buy"))
+                                        (IsExpression (VarExpression "area_garden") (NotExpression (VarExpression "NULL")))
+                                    )
+                                    (IsExpression (VarExpression "rent_monthly") (VarExpression "NULL"))
+                                )
+
+                                (AndExpression
+                                    (AndExpression
+                                        (EqExpression (VarExpression "property_type") (TextExpression "haus_rent"))
+                                        (IsExpression (VarExpression "rent_monthly") (NotExpression (VarExpression "NULL")))
+                                    )
+                                    (IsExpression (VarExpression "price") (VarExpression "NULL"))
+                                )
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. CHECK .. with a <" do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (length(title) < 20);" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression =
+                            LessThanExpression
+                                (CallExpression ("length") [VarExpression "title"])
+                                (IntExpression 20)
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. CHECK .. with a <=" do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (length(title) <= 20);" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression =
+                            LessThanOrEqualToExpression
+                                (CallExpression ("length") [VarExpression "title"])
+                                (IntExpression 20)
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. CHECK .. with a >" do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (length(title) > 20);" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression =
+                            GreaterThanExpression
+                                (CallExpression ("length") [VarExpression "title"])
+                                (IntExpression 20)
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. CHECK .. with a >=" do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT check_title_length CHECK (length(title) >= 20);" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = CheckConstraint
+                        { name = "check_title_length"
+                        , checkExpression =
+                            GreaterThanOrEqualToExpression
+                                (CallExpression ("length") [VarExpression "title"])
+                                (IntExpression 20)
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .." do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (title WITH =, author WITH =);" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE USING btree .." do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE USING btree (title WITH =, author WITH =);" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Just Btree
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE USING gin .." do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE USING gin (title WITH =, author WITH =);" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Just Gin
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE USING gist .." do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE USING gist (title WITH =, author WITH =);" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Just Gist
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. WHERE .." do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (title WITH =, author WITH =) WHERE (title = 'why');" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            , ExcludeConstraintElement { element = "author", operator = "=" }
+                            ]
+                        , predicate = Just $ EqExpression (VarExpression "title") (TextExpression "why")
+                        , indexType = Nothing
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. WHERE .. with various operators" do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT unique_title_by_author EXCLUDE (i1 WITH =, i2 WITH <>, i3 WITH !=, i4 WITH AND, i5 WITH OR) WHERE (title = 'why');" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "i1", operator = "=" }
+                            , ExcludeConstraintElement { element = "i2", operator = "<>" }
+                            , ExcludeConstraintElement { element = "i3", operator = "!=" }
+                            , ExcludeConstraintElement { element = "i4", operator = "AND" }
+                            , ExcludeConstraintElement { element = "i5", operator = "OR" }
+                            ]
+                        , predicate = Just $ EqExpression (VarExpression "title") (TextExpression "why")
+                        , indexType = Nothing
+                        }
+                    , deferrable = Nothing
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. DEFERRABLE" do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT deferrable_unique_title_by_author EXCLUDE (title WITH =) DEFERRABLE;" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "deferrable_unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Just True
+                    , deferrableType = Nothing
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. DEFERRABLE INITIALLY IMMEDIATE" do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT deferrable_unique_title_by_author EXCLUDE (title WITH =) DEFERRABLE INITIALLY IMMEDIATE;" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "deferrable_unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Just True
+                    , deferrableType = Just InitiallyImmediate
+                    }
+
+        it "should parse ALTER TABLE .. ADD CONSTRAINT .. EXCLUDE .. DEFERRABLE INITIALLY DEFERRED" do
+            parseSql "ALTER TABLE posts ADD CONSTRAINT deferrable_unique_title_by_author EXCLUDE (title WITH =) DEFERRABLE INITIALLY DEFERRED;" `shouldBe` AddConstraint
+                    { tableName = "posts"
+                    , constraint = ExcludeConstraint
+                        { name = "deferrable_unique_title_by_author"
+                        , excludeElements =
+                            [ ExcludeConstraintElement { element = "title", operator = "=" }
+                            ]
+                        , predicate = Nothing
+                        , indexType = Nothing
+                        }
+                    , deferrable = Just True
+                    , deferrableType = Just InitiallyDeferred
+                    }
+
+        it "should parse CREATE TYPE .. AS ENUM" do
+            parseSql "CREATE TYPE colors AS ENUM ('yellow', 'red', 'green');" `shouldBe` CreateEnumType { name = "colors", values = ["yellow", "red", "green"] }
+
+        it "should parse CREATE TYPE .. AS ENUM with extra whitespace" do
+            parseSql "CREATE TYPE Numbers AS ENUM (\n\t'One',\t'Two',\t'Three',\t'Four',\t'Five',\t'Six',\t'Seven',\t'Eight',\t'Nine',\t'Ten'\n);" `shouldBe` CreateEnumType { name = "Numbers", values = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"] }
+
+        -- When creating a new Enum Type, it is empty at first.
+        -- Throwing an error for empty Enums renders the visual editor inaccessible.
+        -- Catching empty Enums results in the "Create Enum" UI button being useless.
+        -- Thats why empty Enums will not throw errors.
+        it "should parse CREATE TYPE .. AS ENUM without values" do
+            parseSql "CREATE TYPE colors AS ENUM ();" `shouldBe` CreateEnumType { name = "colors", values = [] }
+
+        it "should parse ALTER TYPE .. ADD VALUE .." do
+            parseSql "ALTER TYPE colors ADD VALUE 'blue';" `shouldBe` AddValueToEnumType { enumName = "colors", newValue = "blue", ifNotExists = False }
+
+        it "should parse ALTER TYPE .. ADD VALUE IF NOT EXISTS .." do
+            parseSql "ALTER TYPE colors ADD VALUE IF NOT EXISTS 'blue';" `shouldBe` AddValueToEnumType { enumName = "colors", newValue = "blue", ifNotExists = True }
+
+        it "should parse a CREATE TABLE with INTEGER / INT / INT4 / SMALLINT / INT2 / BIGINT / INT8 columns" do
+            parseSql "CREATE TABLE ints (int_a INTEGER, int_b INT, int_c int4, smallint_a SMALLINT, smallint_b INT2, bigint_a BIGINT, bigint_b int8);" `shouldBe` StatementCreateTable (table "ints")
+                    { columns =
+                        [ col "int_a" PInt
+                        , col "int_b" PInt
+                        , col "int_c" PInt
+                        , col "smallint_a" PSmallInt
+                        , col "smallint_b" PSmallInt
+                        , col "bigint_a" PBigInt
+                        , col "bigint_b" PBigInt
+                        ]
+                    }
+
+        it "should parse a CREATE TABLE with TIMESTAMP WITH TIMEZONE / TIMESTAMPZ columns" do
+            parseSql "CREATE TABLE timestamps (a TIMESTAMP WITH TIME ZONE, b TIMESTAMPZ);" `shouldBe` StatementCreateTable (table "timestamps")
+                    { columns =
+                        [ col "a" PTimestampWithTimezone
+                        , col "b" PTimestampWithTimezone
+                        ]
+                    }
+
+        it "should parse a CREATE TABLE with BOOLEAN / BOOL columns" do
+            parseSql "CREATE TABLE bools (a BOOLEAN, b BOOL);" `shouldBe` StatementCreateTable (table "bools")
+                    { columns =
+                        [ col "a" PBoolean
+                        , col "b" PBoolean
+                        ]
+                    }
+
+        it "should parse a CREATE TABLE with REAL, FLOAT4, DOUBLE, FLOAT8 columns" do
+            parseSql "CREATE TABLE bools (a REAL, b FLOAT4, c DOUBLE PRECISION, d FLOAT8);" `shouldBe` StatementCreateTable (table "bools")
+                    { columns =
+                        [ col "a" PReal
+                        , col "b" PReal
+                        , col "c" PDouble
+                        , col "d" PDouble
+                        ]
+                    }
+
+        it "should parse a CREATE TABLE with (deprecated) NUMERIC, NUMERIC(x), NUMERIC (x,y), VARYING(n) columns" do
+            parseSql ("CREATE TABLE deprecated_variables (a NUMERIC, b NUMERIC(1), c NUMERIC(1,2), d CHARACTER VARYING(10));") `shouldBe` StatementCreateTable (table "deprecated_variables")
+                    { columns =
+                        [ col "a" (PNumeric Nothing Nothing)
+                        , col "b" (PNumeric (Just 1) Nothing)
+                        , col "c" (PNumeric (Just 1) (Just 2))
+                        , col "d" (PVaryingN (Just 10))
+                        ]
+                    }
+
+        it "should parse a CREATE TABLE statement with a multi-column UNIQUE (a, b) constraint" do
+            parseSql "CREATE TABLE user_followers (id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, user_id UUID NOT NULL, follower_id UUID NOT NULL, UNIQUE(user_id, follower_id));"  `shouldBe` StatementCreateTable (table "user_followers")
+                    { columns =
+                        [ (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }
+                        , (col "user_id" PUUID) { notNull = True }
+                        , (col "follower_id" PUUID) { notNull = True }
+                        ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    , constraints = [ UniqueConstraint { name = Nothing, columnNames = [ "user_id", "follower_id" ] } ]
+                    , unlogged = False
+                    , inherits = Nothing
+                    }
+
+        it "should fail to parse a CREATE TABLE statement with an empty UNIQUE () constraint" do
+            (evaluate (parseSql "CREATE TABLE user_followers (id UUID, UNIQUE());")) `shouldThrow` anyException
+            pure ()
+
+        it "should parse a CREATE TABLE statement with a multi-column PRIMARY KEY (a, b) constraint" do
+            parseSql "CREATE TABLE user_followers (user_id UUID NOT NULL, follower_id UUID NOT NULL, PRIMARY KEY (user_id, follower_id));"  `shouldBe` StatementCreateTable (table "user_followers")
+                    { columns =
+                        [ (col "user_id" PUUID) { notNull = True }
+                        , (col "follower_id" PUUID) { notNull = True }
+                        ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint [ "user_id", "follower_id" ]
+                    }
+
+        it "should fail to parse a CREATE TABLE statement with PRIMARY KEY column and table constraints" do
+            (evaluate (parseSql "CREATE TABLE user_followers (id UUID PRIMARY KEY, PRIMARY KEY(id));")) `shouldThrow` anyException
+            pure ()
+
+        it "should fail to parse a CREATE TABLE statement with an empty PRIMARY KEY () constraint" do
+            (evaluate (parseSql "CREATE TABLE user_followers (id UUID, PRIMARY KEY ());")) `shouldThrow` anyException
+            pure ()
+
+        it "should parse a CREATE TABLE statement with a serial id" do
+            parseSql "CREATE TABLE orders (\n    id SERIAL PRIMARY KEY NOT NULL\n);\n" `shouldBe` StatementCreateTable (table "orders")
+                    { columns = [ (col "id" PSerial) { notNull = True} ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    }
+
+        it "should parse a CREATE TABLE statement with a bigserial id" do
+            parseSql "CREATE TABLE orders (\n    id BIGSERIAL PRIMARY KEY NOT NULL\n);\n" `shouldBe` StatementCreateTable (table "orders")
+                    { columns = [ (col "id" PBigserial) { notNull = True} ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    }
+
+        it "should parse a column with NOT NULL before DEFAULT" do
+            parseSql "CREATE TABLE tasks (is_completed BOOLEAN NOT NULL DEFAULT false);" `shouldBe` StatementCreateTable (table "tasks")
+                    { columns = [ (col "is_completed" PBoolean) { defaultValue = Just (VarExpression "false"), notNull = True } ]
+                    }
+
+        it "should parse column modifiers in mixed order" do
+            parseSql "CREATE TABLE orders (id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL);" `shouldBe` StatementCreateTable (table "orders")
+                    { columns = [ (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True } ]
+                    , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    }
+
+        it "should parse a CREATE TABLE statement with an array column" do
+            parseSql "CREATE TABLE array_tests (\n    pay_by_quarter integer[]\n);\n" `shouldBe` StatementCreateTable (table "array_tests")
+                    { columns = [ col "pay_by_quarter" (PArray PInt) ]
+                    }
+
+        it "should parse a CREATE TABLE statement with a point column" do
+            parseSql "CREATE TABLE points (\n    pos POINT\n);\n" `shouldBe` StatementCreateTable (table "points")
+                    { columns = [ col "pos" PPoint ]
+                    }
+
+        it "should parse a CREATE TABLE statement with a polygon column" do
+            parseSql "CREATE TABLE polygons (\n    poly POLYGON\n);\n" `shouldBe` StatementCreateTable (table "polygons")
+                    { columns = [ col "poly" PPolygon ]
+                    }
+
+        it "should parse a CREATE INDEX statement" do
+            parseSql "CREATE INDEX users_index ON users (user_name);\n" `shouldBe` CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+
+        it "should parse a 'CREATE INDEX .. ON .. USING GIN' statement" do
+            parseSql "CREATE INDEX users_index ON users USING GIN (user_name);\n" `shouldBe` CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Just Gin
+                    }
+
+        it "should parse a 'CREATE INDEX .. ON .. USING btree' statement" do
+            parseSql "CREATE INDEX users_index ON users USING btree (user_name);\n" `shouldBe` CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Just Btree
+                    }
+
+        it "should parse a 'CREATE INDEX .. ON .. USING GIST' statement" do
+            parseSql "CREATE INDEX users_index ON users USING GIST (user_name);\n" `shouldBe` CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Just Gist
+                    }
+
+        it "should parse a CREATE INDEX statement with multiple columns" do
+            parseSql "CREATE INDEX users_index ON users (user_name, project_id);\n" `shouldBe` CreateIndex
+                    { indexName = "users_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns =
+                        [ indexCol (VarExpression "user_name")
+                        , indexCol (VarExpression "project_id")
+                        ]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+        it "should parse a CREATE INDEX statement with a LOWER call" do
+            parseSql "CREATE INDEX users_email_index ON users (LOWER(email));\n" `shouldBe` CreateIndex
+                    { indexName = "users_email_index"
+                    , unique = False
+                    , tableName = "users"
+                    , columns = [indexCol (CallExpression "LOWER" [VarExpression "email"])]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+        it "should parse a CREATE UNIQUE INDEX statement" do
+            parseSql "CREATE UNIQUE INDEX users_index ON users (user_name);\n" `shouldBe` CreateIndex
+                    { indexName = "users_index"
+                    , unique = True
+                    , tableName = "users"
+                    , columns = [indexCol (VarExpression "user_name")]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+
+        it "should parse a CREATE INDEX with column order ASC NULLS FIRST statement" do
+            parseSql "CREATE UNIQUE INDEX users_index ON users (user_name ASC NULLS FIRST);\n" `shouldBe` CreateIndex
+                    { indexName = "users_index"
+                    , unique = True
+                    , tableName = "users"
+                    , columns = [IndexColumn { column = VarExpression "user_name", columnOrder = [Asc, NullsFirst] }]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+
+        it "should parse a CREATE INDEX with column order DESC NULLS LAST statement" do
+            parseSql "CREATE UNIQUE INDEX users_index ON users (user_name DESC NULLS LAST);\n" `shouldBe` CreateIndex
+                    { indexName = "users_index"
+                    , unique = True
+                    , tableName = "users"
+                    , columns = [IndexColumn { column = VarExpression "user_name", columnOrder = [Desc, NullsLast] }]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+
+        it "should parse a CREATE INDEX with a coalesce expression" do
+            parseSql "CREATE UNIQUE INDEX user_invite_uniqueness ON user_invites (organization_id, email, coalesce(expires_at, '0001-01-01 01:01:01-04'));\n" `shouldBe` CreateIndex
+                    { indexName = "user_invite_uniqueness"
+                    , unique = True
+                    , tableName = "user_invites"
+                    , columns =
+                            [ indexCol (VarExpression "organization_id")
+                            , indexCol (VarExpression "email")
+                            , indexCol (CallExpression "coalesce" [VarExpression "expires_at", TextExpression "0001-01-01 01:01:01-04"])
+                            ]
+                    , whereClause = Nothing
+                    , indexType = Nothing
+                    }
+
+        it "should parse a CREATE OR REPLACE FUNCTION ..() RETURNS TRIGGER .." do
+            parseSql "CREATE OR REPLACE FUNCTION notify_did_insert_webrtc_connection() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; $$ language plpgsql;" `shouldBe` (function "notify_did_insert_webrtc_connection")
+                    { functionBody = " BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; "
+                    , orReplace = True
+                    }
+
+        it "should parse a CREATE FUNCTION ..() RETURNS TRIGGER .." do
+            parseSql "CREATE FUNCTION notify_did_insert_webrtc_connection() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; $$ language plpgsql;" `shouldBe` (function "notify_did_insert_webrtc_connection")
+                    { functionBody = " BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; "
+                    }
+
+        it "should parse a CREATE FUNCTION with parameters ..() RETURNS TRIGGER .." do
+            parseSql "CREATE FUNCTION notify_did_insert_webrtc_connection(param1 INT, param2 TEXT) RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; $$ language plpgsql;" `shouldBe` (function "notify_did_insert_webrtc_connection")
+                    { functionArguments = [("param1", PInt), ("param2", PText)]
+                    , functionBody = " BEGIN PERFORM pg_notify('did_insert_webrtc_connection', json_build_object('id', NEW.id, 'floor_id', NEW.floor_id, 'source_user_id', NEW.source_user_id, 'target_user_id', NEW.target_user_id)::text); RETURN NEW; END; "
+                    }
+
+        it "should parse CREATE FUNCTION statements that are outputted by pg_dump" do
+            let sql = cs [plain|
+CREATE FUNCTION public.notify_did_change_projects() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$BEGIN
+    PERFORM pg_notify('did_change_projects', '');
+    RETURN new;END;
+$$;
+            |]
+            parseSql sql `shouldBe` (function "notify_did_change_projects")
+                    { functionBody = "BEGIN\n    PERFORM pg_notify('did_change_projects', '');\n    RETURN new;END;\n"
+                    }
+
+        it "should parse CREATE FUNCTION statements that returns an event_trigger" do
+            let sql = cs [plain|
+                CREATE FUNCTION public.a() RETURNS event_trigger
+                    LANGUAGE plpgsql
+                    AS $$ BEGIN SELECT 1; END; $$;
+            |]
+            parseSql sql `shouldBe` (function "a")
+                    { functionBody = " BEGIN SELECT 1; END; "
+                    , returns = PEventTrigger
+                    }
+
+        it "should parse a CREATE FUNCTION with SECURITY DEFINER" do
+            parseSql "CREATE FUNCTION my_func() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN RETURN NEW; END; $$ ;" `shouldBe` (function "my_func")
+                    { functionBody = " BEGIN RETURN NEW; END; "
+                    , securityDefiner = True
+                    }
+
+        it "should parse a decimal default value with a type-cast" do
+            let sql = "CREATE TABLE a(electricity_unit_price DOUBLE PRECISION DEFAULT 0.17::double precision NOT NULL);"
+            let statements =
+                    [ StatementCreateTable (table "a") { columns = [(col "electricity_unit_price" PDouble) { defaultValue = Just (TypeCastExpression (DoubleExpression 0.17) PDouble), notNull = True }] }
+                    ]
+            parseSqlStatements sql `shouldBe` statements
+
+        it "should parse a integer default value" do
+            let sql = "CREATE TABLE a(electricity_unit_price INT DEFAULT 0 NOT NULL);"
+            let statements =
+                    [ StatementCreateTable (table "a") { columns = [(col "electricity_unit_price" PInt) { defaultValue = Just (IntExpression 0), notNull = True }] }
+                    ]
+            parseSqlStatements sql `shouldBe` statements
+
+        it "should parse a partial index" do
+            parseSql "CREATE UNIQUE INDEX unique_source_id ON listings (source, source_id) WHERE source IS NOT NULL AND source_id IS NOT NULL;" `shouldBe` CreateIndex
+                    { indexName = "unique_source_id"
+                    , unique = True
+                    , tableName = "listings"
+                    , columns =
+                        [ indexCol (VarExpression "source")
+                        , indexCol (VarExpression "source_id")
+                        ]
+                    , whereClause = Just (
+                        AndExpression
+                            (IsExpression (VarExpression "source") (NotExpression (VarExpression "NULL")))
+                            (IsExpression (VarExpression "source_id") (NotExpression (VarExpression "NULL"))))
+                    , indexType = Nothing
+                    }
+
+        it "should parse 'ENABLE ROW LEVEL SECURITY' statements" do
+            parseSql "ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;" `shouldBe` EnableRowLevelSecurity { tableName = "tasks" }
+
+        it "should parse 'CREATE POLICY' statements" do
+            parseSql "CREATE POLICY \"Users can manage their tasks\" ON tasks USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());" `shouldBe`
+                    (policy "Users can manage their tasks" "tasks")
+                    { using = Just (
+                        EqExpression
+                            (VarExpression "user_id")
+                            (CallExpression "ihp_user_id" [])
+                        )
+                    , check = Just (
+                        EqExpression
+                            (VarExpression "user_id")
+                            (CallExpression "ihp_user_id" [])
+                        )
+                    }
+        it "should parse 'ALTER TABLE .. ADD COLUMN' statements" do
+            parseSql "ALTER TABLE a ADD COLUMN b INT NOT NULL;" `shouldBe` AddColumn { tableName = "a", column = (col "b" PInt) { notNull = True } }
+
+        it "should parse 'ALTER TABLE .. DROP COLUMN ..' statements" do
+            parseSql "ALTER TABLE tasks DROP COLUMN description;" `shouldBe` DropColumn { tableName = "tasks", columnName = "description" }
+
+        it "should parse 'ALTER TABLE .. RENAME COLUMN .. TO ..' statements" do
+            parseSql "ALTER TABLE users RENAME COLUMN name TO full_name;" `shouldBe` RenameColumn { tableName = "users", from = "name", to = "full_name" }
+
+        it "should parse 'DROP TABLE ..' statements" do
+            parseSql "DROP TABLE tasks;" `shouldBe` DropTable { tableName = "tasks" }
+
+        it "should parse 'DROP TYPE ..' statements" do
+            parseSql "DROP TYPE colors;" `shouldBe` DropEnumType { name = "colors" }
+
+        it "should parse 'ALTER TABLE .. DROP CONSTRAINT ..' statements" do
+            parseSql "ALTER TABLE tasks DROP CONSTRAINT tasks_title_key;" `shouldBe` DropConstraint { tableName = "tasks", constraintName = "tasks_title_key" }
+
+        it "should parse 'CREATE SEQUENCE ..' statements" do
+            parseSql "CREATE SEQUENCE a;" `shouldBe` CreateSequence { name = "a" }
+
+        it "should parse 'CREATE SEQUENCE ..' statements with qualified name" do
+            parseSql "CREATE SEQUENCE public.a;" `shouldBe` CreateSequence { name = "a" }
+
+        it "should parse 'CREATE SEQUENCE .. AS .. START WITH .. INCREMENT BY .. NO MINVALUE NO MAXVALUE CACHE ..;'" do
+            let sql = [trimming|
+                CREATE SEQUENCE public.a
+                    AS integer
+                    START WITH 1
+                    INCREMENT BY 1
+                    NO MINVALUE
+                    NO MAXVALUE
+                    CACHE 1;
+            |]
+            parseSql sql `shouldBe` CreateSequence { name = "a" }
+
+        it "should parse 'SET' statements" do
+            parseSql "SET statement_timeout = 0;" `shouldBe` Set { name = "statement_timeout", value = IntExpression 0 }
+            parseSql "SET client_encoding = 'UTF8';" `shouldBe` Set { name = "client_encoding", value = TextExpression "UTF8" }
+
+        it "should parse 'SELECT' statements" do
+            parseSql "SELECT pg_catalog.set_config('search_path', '', false);" `shouldBe` SelectStatement { query = "pg_catalog.set_config('search_path', '', false)" }
+        it "should parse 'COMMENT' statements" do
+            parseSql "COMMENT ON EXTENSION \"uuid-ossp\" IS 'generate universally unique identifiers (UUIDs)';" `shouldBe` Comment { content = "ON EXTENSION \"uuid-ossp\" IS 'generate universally unique identifiers (UUIDs)'" }
+
+        it "should parse a column with a default value that has a qualified function call" do
+            let sql = cs [plain|
+                CREATE TABLE a(id UUID DEFAULT public.uuid_generate_v4() NOT NULL);
+            |]
+            let statement = StatementCreateTable (table "a") { columns = [(col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []), notNull = True }] }
+            parseSql sql `shouldBe` statement
+
+
+        it "should parse character varying type casts" do
+            let sql = cs [plain|
+                CREATE TABLE a (
+                    a character varying(510) DEFAULT NULL::character varying
+                );
+            |]
+            let statement = StatementCreateTable (table "a")
+                    { columns = [ (col "a" (PVaryingN (Just 510)))
+                            { defaultValue = Just (TypeCastExpression (VarExpression "NULL") (PVaryingN Nothing))
+                            }
+                        ]
+                    }
+            parseSql sql `shouldBe` statement
+
+        it "should parse empty binary strings" do
+            let sql = cs [plain|
+                CREATE TABLE a (
+                    a bytea DEFAULT '\\x'::bytea NOT NULL
+                );
+            |]
+            let statement = StatementCreateTable (table "a")
+                    { columns = [ (col "a" PBinary)
+                            { defaultValue = Just (TypeCastExpression (TextExpression "") PBinary)
+                            , notNull = True
+                            }
+                        ]
+                    }
+            parseSql sql `shouldBe` statement
+        it "should parse a pg_dump header" do
+            let sql = cs [plain|
+--
+-- PostgreSQL database dump
+--
+
+-- Dumped from database version 13.3
+-- Dumped by pg_dump version 13.3
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+
+--
+-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -
+--
+
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
+
+
+--
+-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: -
+--
+
+COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
+
+
+            |]
+
+            let statements =
+                    [ Comment {content = ""}
+                    , Comment {content = " PostgreSQL database dump"}
+                    , Comment {content = ""}
+                    , Comment {content = " Dumped from database version 13.3"}
+                    , Comment {content = " Dumped by pg_dump version 13.3"}
+                    , Set {name = "statement_timeout", value = IntExpression 0}
+                    , Set {name = "lock_timeout", value = IntExpression 0}
+                    , Set {name = "idle_in_transaction_session_timeout", value = IntExpression 0}
+                    , Set {name = "client_encoding", value = TextExpression "UTF8"}
+                    , Set {name = "standard_conforming_strings", value = VarExpression "on"}
+                    , SelectStatement {query = "pg_catalog.set_config('search_path', '', false)"}
+                    , Set {name = "check_function_bodies", value = VarExpression "false"}
+                    , Set {name = "xmloption", value = VarExpression "content"}
+                    , Set {name = "client_min_messages", value = VarExpression "warning"}
+                    , Set {name = "row_security", value = VarExpression "off"}
+                    , Comment {content = ""}
+                    , Comment {content = " Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -"}
+                    , Comment {content = ""}
+                    , CreateExtension {name = "uuid-ossp", ifNotExists = True}
+                    , Comment {content = ""}
+                    , Comment {content = " Name: EXTENSION \"uuid-ossp\"; Type: COMMENT; Schema: -; Owner: -"}
+                    , Comment {content = ""}
+                    , Comment {content = "ON EXTENSION \"uuid-ossp\" IS 'generate universally unique identifiers (UUIDs)'"}
+                    ]
+            parseSqlStatements sql `shouldBe` statements
+
+        it "should parse 'DROP INDEX ..' statements" do
+            parseSql "DROP INDEX a;" `shouldBe` DropIndex { indexName = "a" }
+
+        it "should parse 'ALTER TABLE .. ALTER COLUMN .. DROP NOT NULL' statements" do
+            parseSql "ALTER TABLE a ALTER COLUMN b DROP NOT NULL;" `shouldBe` DropNotNull { tableName = "a", columnName = "b" }
+
+        it "should parse 'ALTER TABLE .. ALTER COLUMN .. SET NOT NULL' statements" do
+            parseSql "ALTER TABLE a ALTER COLUMN b SET NOT NULL;" `shouldBe` SetNotNull { tableName = "a", columnName = "b" }
+
+        it "should parse 'ALTER TABLE .. ALTER COLUMN .. SET DEFAULT ..' statements" do
+            parseSql "ALTER TABLE a ALTER COLUMN b SET DEFAULT null;" `shouldBe` SetDefaultValue { tableName = "a", columnName = "b", value = VarExpression "null" }
+
+        it "should parse 'ALTER TABLE .. ALTER COLUMN .. DROP DEFAULT' statements" do
+            parseSql "ALTER TABLE a ALTER COLUMN b DROP DEFAULT;" `shouldBe` DropDefaultValue { tableName = "a", columnName = "b" }
+
+        it "should parse 'ALTER TABLE .. RENAME TO ..' statements" do
+            parseSql "ALTER TABLE profiles RENAME TO users;" `shouldBe` RenameTable { from = "profiles", to = "users" }
+
+        it "should parse 'DROP POLICY .. ON ..' statements" do
+            parseSql "DROP POLICY \"Users can manage their todos\" ON todos;" `shouldBe` DropPolicy { tableName = "todos", policyName = "Users can manage their todos" }
+
+        it "should parse 'CREATE POLICY .. FOR SELECT' statements" do
+            let sql = cs [plain|CREATE POLICY "Messages are public" ON messages FOR SELECT USING (true);|]
+            parseSql sql `shouldBe` CreatePolicy
+                    { name = "Messages are public"
+                    , action = Just PolicyForSelect
+                    , tableName = "messages"
+                    , using = Just (VarExpression "true")
+                    , check = Nothing
+                    }
+
+        it "should parse policies with an EXISTS condition" do
+            let sql = cs [plain|CREATE POLICY "Users can manage their project's migrations" ON migrations USING (EXISTS (SELECT 1 FROM projects WHERE id = project_id)) WITH CHECK (EXISTS (SELECT 1 FROM projects WHERE id = project_id));|]
+            parseSql sql `shouldBe`
+                    (policy "Users can manage their project's migrations" "migrations")
+                    { using = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = VarExpression "projects", alias = Nothing, whereClause = EqExpression (VarExpression "id") (VarExpression "project_id")})))
+                    , check = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = VarExpression "projects", alias = Nothing, whereClause = EqExpression (VarExpression "id") (VarExpression "project_id")})))
+                    }
+
+        it "should parse policies with an EXISTS condition and a qualified table name" do
+            let sql = cs [plain|CREATE POLICY "Users can manage their project's migrations" ON migrations USING (EXISTS (SELECT 1 FROM public.projects WHERE projects.id = migrations.project_id)) WITH CHECK (EXISTS (SELECT 1 FROM public.projects WHERE projects.id = migrations.project_id));|]
+            parseSql sql `shouldBe`
+                    (policy "Users can manage their project's migrations" "migrations")
+                    { using = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = DotExpression (VarExpression "public") "projects", alias = Nothing, whereClause = EqExpression (DotExpression (VarExpression "projects") "id") (DotExpression (VarExpression "migrations") "project_id")})))
+                    , check = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = DotExpression (VarExpression "public") "projects", alias = Nothing, whereClause = EqExpression (DotExpression (VarExpression "projects") "id") (DotExpression (VarExpression "migrations") "project_id")})))
+                    }
+
+        it "should parse a call expression with multiple arguments" do
+            let sql = cs [plain|ALTER TABLE a ADD CONSTRAINT source CHECK (num_nonnulls(a, b, c) = 1);|]
+            parseSql sql `shouldBe`  AddConstraint
+                { tableName = "a"
+                , constraint = CheckConstraint
+                    { name = Just "source"
+                    , checkExpression = EqExpression (CallExpression "num_nonnulls" [VarExpression "a",VarExpression "b",VarExpression "c"]) (IntExpression 1)
+                    }
+                , deferrable = Nothing
+                , deferrableType = Nothing
+                }
+
+        it "should parse 'CREATE TRIGGER .. AFTER INSERT ON .. FOR EACH ROW EXECUTE ..;' statements" do
+            parseSql "CREATE TRIGGER call_test_function_for_new_users AFTER INSERT ON public.users FOR EACH ROW EXECUTE FUNCTION call_test_function('hello');" `shouldBe` CreateTrigger
+                    { name = "call_test_function_for_new_users"
+                    , eventWhen = After
+                    , event = [TriggerOnInsert]
+                    , tableName = "users"
+                    , for = ForEachRow
+                    , whenCondition = Nothing
+                    , functionName = "call_test_function"
+                    , arguments = [TextExpression "hello"]
+                    }
+
+        it "should parse 'CREATE TRIGGER .. AFTER INSERT OR UPDATE ON ..' statements" do
+            parseSql "CREATE TRIGGER my_trigger AFTER INSERT OR UPDATE ON public.posts FOR EACH ROW EXECUTE FUNCTION my_function();" `shouldBe` CreateTrigger
+                    { name = "my_trigger"
+                    , eventWhen = After
+                    , event = [TriggerOnInsert, TriggerOnUpdate]
+                    , tableName = "posts"
+                    , for = ForEachRow
+                    , whenCondition = Nothing
+                    , functionName = "my_function"
+                    , arguments = []
+                    }
+
+        it "should parse 'CREATE EVENT TRIGGER ..' statements" do
+            let sql = cs [plain|
+                CREATE EVENT TRIGGER trigger_update_schema
+                ON ddl_command_end
+                WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE', 'DROP TABLE')
+                EXECUTE FUNCTION update_tables_and_columns();
+            |]
+            parseSql sql `shouldBe` CreateEventTrigger
+                    { name = "trigger_update_schema"
+                    , eventOn = "ddl_command_end"
+                    , whenCondition =  Just (InExpression (VarExpression "TAG") (InArrayExpression [TextExpression "CREATE TABLE", TextExpression "ALTER TABLE", TextExpression "DROP TABLE"]))
+                    , functionName = "update_tables_and_columns"
+                    , arguments = []
+                    }
+
+        it "should parse 'ALTER SEQUENCE ..' statements" do
+            let sql = cs [plain|ALTER SEQUENCE public.a OWNED BY public.b.serial_number;|]
+            parseSql sql `shouldBe` UnknownStatement { raw = "ALTER SEQUENCE public.a OWNED BY public.b.serial_number" }
+
+        it "should parse positive IntExpression's" do
+            parseExpression "1" `shouldBe` (IntExpression 1)
+
+        it "should parse negative IntExpression's" do
+            parseExpression "-1" `shouldBe` (IntExpression (-1))
+
+        it "should parse positive DoubleExpression's" do
+            parseExpression "1.337" `shouldBe` (DoubleExpression 1.337)
+
+        it "should parse negative DoubleExpression's" do
+            parseExpression "-1.337" `shouldBe` (DoubleExpression (-1.337))
+
+        it "should parse lower-cased SELECT expressions" do
+            parseExpression "(select company_id from users where id = ihp_user_id())" `shouldBe` SelectExpression (Select {columns = [VarExpression "company_id"], from = VarExpression "users", alias = Nothing, whereClause = EqExpression (VarExpression "id") (CallExpression "ihp_user_id" [])})
+
+        it "should parse policies with an alias in the USING expression" do
+            let sql = cs [plain|
+                CREATE POLICY "Users can see other users in their company" ON public.users USING ((company_id = ( SELECT users_1.company_id
+                   FROM public.users users_1
+                  WHERE (users_1.id = public.ihp_user_id()))));
+            |]
+            parseSql sql `shouldBe`
+                    (policy "Users can see other users in their company" "users")
+                    { using = Just (EqExpression (VarExpression "company_id") (SelectExpression (Select {columns = [DotExpression (VarExpression "users_1") "company_id"], from = DotExpression (VarExpression "public") "users", alias = Just "users_1", whereClause = EqExpression (DotExpression (VarExpression "users_1") "id") (CallExpression "ihp_user_id" [])})))
+                    }
+
+        it "should parse 'BEGIN' statements" do
+            let sql = cs [plain|BEGIN;|]
+            parseSql sql `shouldBe` Begin
+
+        it "should parse 'COMMIT' statements" do
+            let sql = cs [plain|COMMIT;|]
+            parseSql sql `shouldBe` Commit
+        
+        it "should parse 'DROP FUNCTION ..' statements" do
+            let sql = cs [plain|DROP FUNCTION my_function;|]
+            parseSql sql `shouldBe` DropFunction { functionName = "my_function" }
+        
+        it "should parse 'CREATE TABLE ..' statements when the table name starts with public" do
+            let sql = cs [plain|CREATE TABLE public_variables (id UUID);|]
+            parseSql sql `shouldBe` StatementCreateTable (table "public_variables") { columns = [col "id" PUUID] }
+
+        it "should parse an 'CREATE UNLOGGED TABLE' statement" do
+            parseSql "CREATE UNLOGGED TABLE pg_large_notifications ();"  `shouldBe` StatementCreateTable (table "pg_large_notifications") { unlogged = True }
+
+        it "should ignore restrict lines" do
+            let sql = [trimming|
+                \restrict LjgPjBgHVdXUE0a19ZenYCd3Zs2dsdUxghYk15OGwb4zzkNflnbsZ4rQo7Eqm5G
+                \unrestrict LjgPjBgHVdXUE0a19ZenYCd3Zs2dsdUxghYk15OGwb4zzkNflnbsZ4rQo7Eqm5G
+            |]
+            parseSqlStatements sql `shouldBe` [Comment { content = "" }, Comment { content = "" }]
+
+parseSql :: Text -> Statement
+parseSql sql = let [statement] = parseSqlStatements sql in statement
+
+parseSqlStatements :: Text -> [Statement]
+parseSqlStatements sql =
+    case Megaparsec.runParser Parser.parseDDL "input" sql of
+            Left parserError -> error (cs $ Megaparsec.errorBundlePretty parserError) -- For better error reporting in hspec
+            Right statements -> statements
+
+parseExpression :: Text -> Expression
+parseExpression sql =
+    case Megaparsec.runParser Parser.expression "input" sql of
+            Left parserError -> error (cs $ Megaparsec.errorBundlePretty parserError) -- For better error reporting in hspec
+            Right expression -> expression
diff --git a/Test/IDE/SchemaDesigner/SchemaOperationsSpec.hs b/Test/IDE/SchemaDesigner/SchemaOperationsSpec.hs
new file mode 100644
--- /dev/null
+++ b/Test/IDE/SchemaDesigner/SchemaOperationsSpec.hs
@@ -0,0 +1,499 @@
+module Test.IDE.SchemaDesigner.SchemaOperationsSpec where
+
+import Test.Hspec
+import IHP.Prelude
+import IHP.Postgres.Types
+import qualified IHP.IDE.SchemaDesigner.SchemaOperations as SchemaOperations
+import qualified IHP.Postgres.Parser as Parser
+import qualified Text.Megaparsec as Megaparsec
+
+tests = do
+    describe "IHP.IDE.SchemaDesigner.SchemaOperations" do
+        let tableA = StatementCreateTable (table "a")
+        let tableB = StatementCreateTable (table "b")
+        let enumA = CreateEnumType { name = "enumA", values = [] }
+        let enumB = CreateEnumType { name = "enumB", values = [] }
+        let comment = Comment { content = "comment" }
+        describe "addEnum" do
+            it "should prepend the enum" do
+                let inputSchema = [tableA, tableB]
+                let expectedSchema = [enumA, tableA, tableB ]
+
+                (SchemaOperations.addEnum "enumA" inputSchema) `shouldBe` expectedSchema
+
+            it "should prepend the enum, but after existing enums" do
+
+                let inputSchema = [enumA, tableA]
+                let expectedSchema = [enumA, enumB, tableA]
+
+                (SchemaOperations.addEnum "enumB" inputSchema) `shouldBe` expectedSchema
+            
+            it "should deal with the empty case" do
+                let inputSchema = []
+                let expectedSchema = [enumA]
+
+                (SchemaOperations.addEnum "enumA" inputSchema) `shouldBe` expectedSchema
+
+            it "should ignore comments" do
+                let inputSchema = [comment, enumA, tableA]
+                let expectedSchema = [comment, enumA, enumB, tableA]
+
+                (SchemaOperations.addEnum "enumB" inputSchema) `shouldBe` expectedSchema
+
+        describe "enableRowLevelSecurity" do
+            it "should enable row level security if not enabled yet" do
+                let inputSchema = [tableA]
+                let expectedSchema = [tableA, EnableRowLevelSecurity { tableName = "a"} ]
+
+                (SchemaOperations.enableRowLevelSecurity "a" inputSchema) `shouldBe` expectedSchema
+            
+            it "should not do anything if already enabled" do
+                let inputSchema = [tableA, EnableRowLevelSecurity { tableName = "a"} ]
+                let expectedSchema = [tableA, EnableRowLevelSecurity { tableName = "a"} ]
+
+                (SchemaOperations.enableRowLevelSecurity "a" inputSchema) `shouldBe` expectedSchema
+        
+        describe "disableRowLevelSecurity" do
+            it "should disable row level security if enabled" do
+                let inputSchema = [tableA, EnableRowLevelSecurity { tableName = "a"}]
+                let expectedSchema = [tableA]
+
+                (SchemaOperations.disableRowLevelSecurity "a" inputSchema) `shouldBe` expectedSchema
+            
+            it "should not do anything if the row level security is not enabled" do
+                let inputSchema = [tableA]
+                let expectedSchema = [tableA]
+
+                (SchemaOperations.disableRowLevelSecurity "a" inputSchema) `shouldBe` expectedSchema
+        
+        describe "disableRowLevelSecurityIfNoPolicies" do
+            it "should disable row level security if there's no policy" do
+                let inputSchema = [tableA, EnableRowLevelSecurity { tableName = "a"}]
+                let expectedSchema = [tableA]
+
+                (SchemaOperations.disableRowLevelSecurityIfNoPolicies "a" inputSchema) `shouldBe` expectedSchema
+            
+            it "should not do anything if the row level security is not enabled" do
+                let inputSchema = [tableA]
+
+                (SchemaOperations.disableRowLevelSecurityIfNoPolicies "a" inputSchema) `shouldBe` inputSchema
+            
+            it "should not do anything if there's a policy" do
+                let p = policy "p" "a"
+                let inputSchema = [tableA, EnableRowLevelSecurity { tableName = "a"}, p]
+
+                (SchemaOperations.disableRowLevelSecurityIfNoPolicies "a" inputSchema) `shouldBe` inputSchema
+
+        describe "deleteTable" do
+            it "delete a table with all it's indices, constraints, policies, enable RLS statements, triggers" do
+                let inputSchema = parseSqlStatements [trimming|
+                    CREATE TABLE users ();
+                    CREATE TABLE tasks ();
+                    CREATE INDEX tasks_user_id_index ON tasks (user_id);
+                    ALTER TABLE tasks ADD CONSTRAINT tasks_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                    CREATE POLICY "Users can manage their tasks" ON tasks USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                    ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
+                    CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE ON tasks FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
+                |]
+                let outputSchema = parseSqlStatements [trimming|
+                    CREATE TABLE users ();
+                |]
+
+                SchemaOperations.deleteTable "tasks" inputSchema `shouldBe` outputSchema
+
+        describe "suggestPolicy" do
+            it "should suggest a policy if a user_id column exists" do
+                let postsTable = StatementCreateTable (table "posts")
+                                { columns =
+                                    [ (col "user_id" PUUID) { notNull = True }
+                                    ]
+                                }
+                let schema = [postsTable]
+                let expectedPolicy = (policy "Users can manage their posts" "posts")
+                        { using = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" []))
+                        , check = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" []))
+                        }
+
+                SchemaOperations.suggestPolicy schema postsTable `shouldBe` expectedPolicy
+
+            it "should suggest an empty policy if no user_id column exists" do
+                let postsTable = StatementCreateTable (table "posts")
+                                { columns =
+                                    [ (col "title" PText) { notNull = True }
+                                    ]
+                                }
+                let schema = [postsTable]
+                let expectedPolicy = policy "" "posts"
+
+                SchemaOperations.suggestPolicy schema postsTable `shouldBe` expectedPolicy
+
+            it "should suggest a policy if it can find a one hop path to a user_id column" do
+                let tasksTable = StatementCreateTable (table "tasks")
+                                { columns =
+                                    [ (col "task_list_id" PUUID) { notNull = True }
+                                    ]
+                                }
+                let taskListsTable = StatementCreateTable (table "task_lists")
+                                { columns =
+                                    [ (col "user_id" PUUID) { notNull = True }
+                                    ]
+                                }
+                let schema =
+                            [ tasksTable
+                            , taskListsTable
+                            , AddConstraint { tableName = "tasks", constraint = ForeignKeyConstraint { name = "tasks_ref_task_lists", columnName = "task_list_id", referenceTable = "task_lists", referenceColumn = Nothing, onDelete = Nothing }, deferrable = Nothing, deferrableType = Nothing }
+                            ]
+                let expectedPolicy = (policy "Users can manage the tasks if they can see the TaskList" "tasks")
+                        { using = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = DotExpression (VarExpression "public") "task_lists", alias = Nothing, whereClause = EqExpression (DotExpression (VarExpression "task_lists") "id") (DotExpression (VarExpression "tasks") "task_list_id")})))
+                        , check = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = DotExpression (VarExpression "public") "task_lists", alias = Nothing, whereClause = EqExpression (DotExpression (VarExpression "task_lists") "id") (DotExpression (VarExpression "tasks") "task_list_id")})))
+                        }
+
+                SchemaOperations.suggestPolicy schema tasksTable `shouldBe` expectedPolicy
+        describe "addColumn" do
+            it "should add an index if withIndex = true" do
+                let inputSchema = [tableA]
+
+                let tableAWithCreatedAt = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "created_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
+                            ]
+                            }
+                let index = CreateIndex { indexName = "a_created_at_index", unique = False, tableName = "a", columns = [indexCol (VarExpression "created_at")], whereClause = Nothing, indexType = Nothing }
+
+                let expectedSchema = [tableAWithCreatedAt, index]
+
+                let options = SchemaOperations.AddColumnOptions
+                        { tableName = "a"
+                        , columnName = "created_at"
+                        , columnType = PTimestampWithTimezone
+                        , defaultValue = Just (CallExpression "NOW" [])
+                        , isArray = False
+                        , allowNull = False
+                        , isUnique = False
+                        , isReference = False
+                        , referenceTable = Nothing
+                        , primaryKey = False
+                        , withIndex = True
+                        , autoPolicy = False
+                        }
+
+                (SchemaOperations.addColumn options inputSchema) `shouldBe` expectedSchema
+            
+            it "should add a trigger to updated_at columns" do
+                let inputSchema = [tableA]
+
+                let tableAWithCreatedAt = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "updated_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
+                            ]
+                            }
+
+                let setUpdatedAtFn = (function "set_updated_at_to_now")
+                            { functionBody = "\nBEGIN\n    NEW.updated_at = NOW();\n    RETURN NEW;\nEND;\n"
+                            }
+                let trigger = CreateTrigger
+                            { name = "update_a_updated_at"
+                            , eventWhen = Before
+                            , event = [TriggerOnUpdate]
+                            , tableName = "a"
+                            , for = ForEachRow
+                            , whenCondition = Nothing
+                            , functionName = "set_updated_at_to_now"
+                            , arguments = []
+                            }
+
+                let expectedSchema = [setUpdatedAtFn, tableAWithCreatedAt, trigger]
+                
+                let options = SchemaOperations.AddColumnOptions
+                        { tableName = "a"
+                        , columnName = "updated_at"
+                        , columnType = PTimestampWithTimezone
+                        , defaultValue = Just (CallExpression "NOW" [])
+                        , isArray = False
+                        , allowNull = False
+                        , isUnique = False
+                        , isReference = False
+                        , referenceTable = Nothing
+                        , primaryKey = False
+                        , withIndex = False
+                        , autoPolicy = False
+                        }
+
+                (SchemaOperations.addColumn options inputSchema) `shouldBe` expectedSchema
+            
+            it "should add a policy if autoPolicy = true" do
+                let inputSchema = [tableA]
+
+                let tableAWithCreatedAt = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "user_id" PUUID) { notNull = True }
+                            ]
+                            }
+
+                let index = CreateIndex
+                        { indexName = "a_user_id_index"
+                        , unique = False
+                        , tableName = "a"
+                        , columns = [indexCol (VarExpression "user_id")]
+                        , whereClause = Nothing
+                        , indexType = Nothing
+                        }
+                let constraint = AddConstraint
+                        { tableName = "a"
+                        , constraint = ForeignKeyConstraint { name = Just "a_ref_user_id", columnName = "user_id", referenceTable = "users", referenceColumn = Just "id", onDelete = Just NoAction }
+                        , deferrable = Nothing
+                        , deferrableType = Nothing
+                        }
+                let enableRLS = EnableRowLevelSecurity { tableName = "a" }
+                let p = (policy "Users can manage their a" "a")
+                        { using = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" []))
+                        , check = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" []))
+                        }
+
+                let expectedSchema = [tableAWithCreatedAt, index, constraint, enableRLS, p]
+                
+                let options = SchemaOperations.AddColumnOptions
+                        { tableName = "a"
+                        , columnName = "user_id"
+                        , columnType = PUUID
+                        , defaultValue = Nothing
+                        , isArray = False
+                        , allowNull = False
+                        , isUnique = False
+                        , isReference = True
+                        , referenceTable = Just "users"
+                        , primaryKey = False
+                        , withIndex = False
+                        , autoPolicy = True
+                        }
+
+                (SchemaOperations.addColumn options inputSchema) `shouldBe` expectedSchema
+
+        describe "deleteColumn" do
+            it "should delete an referenced index" do
+                let tableAWithCreatedAt = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "created_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
+                            ]
+                            }
+                let index = CreateIndex { indexName = "a_created_at_index", unique = False, tableName = "a", columns = [indexCol (VarExpression "created_at")], whereClause = Nothing, indexType = Nothing }
+
+                let inputSchema = [tableAWithCreatedAt, index]
+                let expectedSchema = [tableA]
+                
+                let options = SchemaOperations.DeleteColumnOptions
+                        { tableName = "a"
+                        , columnName = "created_at"
+                        , columnId = 0
+                        }
+
+                (SchemaOperations.deleteColumn options inputSchema) `shouldBe` expectedSchema
+            
+            it "should delete a updated_at trigger" do
+                let tableAWithCreatedAt = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "updated_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
+                            ]
+                            }
+
+                let setUpdatedAtFn = (function "set_updated_at_to_now")
+                            { functionBody = "\nBEGIN\n    NEW.updated_at = NOW();\n    RETURN NEW;\nEND;\n"
+                            }
+                let trigger = CreateTrigger
+                            { name = "update_a_updated_at"
+                            , eventWhen = Before
+                            , event = [TriggerOnUpdate]
+                            , tableName = "a"
+                            , for = ForEachRow
+                            , whenCondition = Nothing
+                            , functionName = "set_updated_at_to_now"
+                            , arguments = []
+                            }
+
+                let inputSchema = [setUpdatedAtFn, tableAWithCreatedAt, trigger]
+                let expectedSchema = [setUpdatedAtFn, tableA]
+                
+                let options = SchemaOperations.DeleteColumnOptions
+                        { tableName = "a"
+                        , columnName = "updated_at"
+                        , columnId = 0
+                        }
+
+                (SchemaOperations.deleteColumn options inputSchema) `shouldBe` expectedSchema
+            
+            it "should delete an referenced policy" do
+                let tableAWithUserId = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "user_id" PUUID) { defaultValue = Just (CallExpression "ihp_user_id" []), notNull = True }
+                            ]
+                            }
+                let p = (policy "a_policy" "a") { using = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" [])) }
+
+                let inputSchema = [tableAWithUserId, p]
+                let expectedSchema = [tableA]
+                
+                let options = SchemaOperations.DeleteColumnOptions
+                        { tableName = "a"
+                        , columnName = "user_id"
+                        , columnId = 0
+                        }
+
+                (SchemaOperations.deleteColumn options inputSchema) `shouldBe` expectedSchema
+        describe "update" do
+            it "update a column's name, type, default value and not null" do
+                let tableAWithCreatedAt = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "updated_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
+                            ]
+                            }
+
+                let tableAWithUpdatedColumn = StatementCreateTable (table "a")
+                            { columns = [
+                                    col "created_at2" PText
+                            ]
+                            }
+
+                let inputSchema = [tableAWithCreatedAt]
+                let expectedSchema = [tableAWithUpdatedColumn]
+                
+                let options = SchemaOperations.UpdateColumnOptions
+                        { tableName = "a"
+                        , columnName = "created_at2"
+                        , columnType = PText
+                        , defaultValue = Nothing
+                        , isArray = False
+                        , allowNull = True
+                        , isUnique = False
+                        , primaryKey = False
+                        , columnId = 0
+                        }
+
+                (SchemaOperations.updateColumn options inputSchema) `shouldBe` expectedSchema
+            it "updates a primary key" do
+                let tableWithPK = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "id2" PUUID) { notNull = True }
+                            ]
+                            , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                            }
+
+                let tableWithoutPK = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "id" PUUID) { notNull = True }
+                            ]
+                            }
+
+                let inputSchema = [tableWithoutPK]
+                let expectedSchema = [tableWithPK]
+                
+                let options = SchemaOperations.UpdateColumnOptions
+                        { tableName = "a"
+                        , columnName = "id2"
+                        , columnType = PUUID
+                        , defaultValue = Nothing
+                        , isArray = False
+                        , allowNull = False
+                        , isUnique = False
+                        , primaryKey = True
+                        , columnId = 0
+                        }
+
+                (SchemaOperations.updateColumn options inputSchema) `shouldBe` expectedSchema
+            it "updates referenced foreign key constraints" do
+                let tasksTable = StatementCreateTable (table "tasks")
+                                { columns =
+                                    [ (col "task_list_id" PUUID) { notNull = True }
+                                    ]
+                                }
+                let taskListsTable = StatementCreateTable (table "task_lists")
+                                { columns =
+                                    [ (col "user_id" PUUID) { notNull = True }
+                                    ]
+                                }
+                let inputSchema =
+                            [ tasksTable
+                            , taskListsTable
+                            , AddConstraint { tableName = "tasks", constraint = ForeignKeyConstraint { name = "tasks_ref_task_lists", columnName = "task_list_id", referenceTable = "task_lists", referenceColumn = Nothing, onDelete = Nothing }, deferrable = Nothing, deferrableType = Nothing }
+                            ]
+
+                let tasksTable' = StatementCreateTable (table "tasks")
+                                { columns =
+                                    [ (col "list_id" PUUID) { notNull = True }
+                                    ]
+                                }
+                let expectedSchema =
+                            [ tasksTable'
+                            , taskListsTable
+                            , AddConstraint { tableName = "tasks", constraint = ForeignKeyConstraint { name = "tasks_ref_task_lists", columnName = "list_id", referenceTable = "task_lists", referenceColumn = Nothing, onDelete = Nothing }, deferrable = Nothing, deferrableType = Nothing }
+                            ]
+                
+                let options = SchemaOperations.UpdateColumnOptions
+                        { tableName = "tasks"
+                        , columnName = "list_id"
+                        , columnType = PUUID
+                        , defaultValue = Nothing
+                        , isArray = False
+                        , allowNull = False
+                        , isUnique = False
+                        , primaryKey = False
+                        , columnId = 0
+                        }
+
+                (SchemaOperations.updateColumn options inputSchema) `shouldBe` expectedSchema
+            it "update a column's indexes" do
+                let tableAWithCreatedAt = StatementCreateTable (table "a")
+                            { columns = [
+                                    (col "updated_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
+                            ]
+                            }
+                let index = CreateIndex { indexName = "a_updated_at_index", unique = False, tableName = "a", columns = [indexCol (VarExpression "updated_at")], whereClause = Nothing, indexType = Nothing }
+
+                let tableAWithUpdatedColumn = StatementCreateTable (table "a")
+                            { columns = [
+                                    col "created_at" PText
+                            ]
+                            }
+                let indexUpdated = CreateIndex { indexName = "a_created_at_index", unique = False, tableName = "a", columns = [indexCol (VarExpression "created_at")], whereClause = Nothing, indexType = Nothing }
+
+                let inputSchema = [tableAWithCreatedAt, index]
+                let expectedSchema = [tableAWithUpdatedColumn, indexUpdated]
+                
+                let options = SchemaOperations.UpdateColumnOptions
+                        { tableName = "a"
+                        , columnName = "created_at"
+                        , columnType = PText
+                        , defaultValue = Nothing
+                        , isArray = False
+                        , allowNull = True
+                        , isUnique = False
+                        , primaryKey = False
+                        , columnId = 0
+                        }
+
+                (SchemaOperations.updateColumn options inputSchema) `shouldBe` expectedSchema
+        describe "updateTable" do
+            it "renames a table with all it's indices, constraints, policies, enable RLS statements, triggers" do
+                let inputSchema = parseSqlStatements [trimming|
+                    CREATE TABLE tasks ();
+                    CREATE INDEX tasks_user_id_index ON tasks (user_id);
+                    ALTER TABLE tasks ADD CONSTRAINT tasks_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                    CREATE POLICY "Users can manage their tasks" ON tasks USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                    ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
+                    CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE ON tasks FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
+                |]
+                let outputSchema = parseSqlStatements [trimming|
+                    CREATE TABLE todos ();
+                    CREATE INDEX todos_user_id_index ON todos (user_id);
+                    ALTER TABLE todos ADD CONSTRAINT todos_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                    CREATE POLICY "Users can manage their todos" ON todos USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
+                    ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
+                    CREATE TRIGGER update_todos_updated_at BEFORE UPDATE ON todos FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
+                |]
+
+                SchemaOperations.updateTable 0 "todos" inputSchema `shouldBe` outputSchema
+
+parseSqlStatements :: Text -> [Statement]
+parseSqlStatements sql =
+    case Megaparsec.runParser Parser.parseDDL "input" sql of
+            Left parserError -> error (cs $ Megaparsec.errorBundlePretty parserError) -- For better error reporting in hspec
+            Right statements -> statements
diff --git a/Test/Main.hs b/Test/Main.hs
new file mode 100644
--- /dev/null
+++ b/Test/Main.hs
@@ -0,0 +1,38 @@
+module Main where
+
+import Test.Hspec
+import IHP.Prelude
+
+import qualified Test.IDE.SchemaDesigner.CompilerSpec
+import qualified Test.IDE.SchemaDesigner.ParserSpec
+import qualified Test.IDE.SchemaDesigner.Controller.EnumValuesSpec
+import qualified Test.IDE.SchemaDesigner.Controller.HelperSpec
+import qualified Test.IDE.SchemaDesigner.Controller.ValidationSpec
+import qualified Test.IDE.SchemaDesigner.SchemaOperationsSpec
+import qualified Test.IDE.CodeGeneration.ControllerGenerator
+import qualified Test.IDE.CodeGeneration.ViewGenerator
+import qualified Test.IDE.CodeGeneration.MailGenerator
+import qualified Test.IDE.CodeGeneration.JobGenerator
+import qualified Test.IDE.CodeGeneration.MigrationGenerator
+import qualified Test.SchemaCompilerSpec
+import qualified Test.IDE.ToolServer.MiddlewareSpec
+import qualified Test.IDE.Logs.ControllerSpec
+import qualified Test.ServerSpec
+
+main :: IO ()
+main = hspec do
+    Test.IDE.SchemaDesigner.CompilerSpec.tests
+    Test.IDE.SchemaDesigner.ParserSpec.tests
+    Test.IDE.SchemaDesigner.Controller.EnumValuesSpec.tests
+    Test.IDE.SchemaDesigner.Controller.HelperSpec.tests
+    Test.IDE.SchemaDesigner.Controller.ValidationSpec.tests
+    Test.IDE.CodeGeneration.ControllerGenerator.tests
+    Test.IDE.CodeGeneration.ViewGenerator.tests
+    Test.IDE.CodeGeneration.MailGenerator.tests
+    Test.IDE.CodeGeneration.JobGenerator.tests
+    Test.IDE.SchemaDesigner.SchemaOperationsSpec.tests
+    Test.IDE.CodeGeneration.MigrationGenerator.tests
+    Test.SchemaCompilerSpec.tests
+    Test.IDE.ToolServer.MiddlewareSpec.tests
+    Test.IDE.Logs.ControllerSpec.tests
+    Test.ServerSpec.tests
diff --git a/Test/SchemaCompilerSpec.hs b/Test/SchemaCompilerSpec.hs
new file mode 100644
--- /dev/null
+++ b/Test/SchemaCompilerSpec.hs
@@ -0,0 +1,1339 @@
+{-|
+Module: Test.SchemaCompilerSpec
+Copyright: (c) digitally induced GmbH, 2020
+-}
+module Test.SchemaCompilerSpec where
+
+import Test.Hspec
+import IHP.Prelude
+import IHP.SchemaCompiler
+import IHP.Postgres.Types
+import qualified Data.Text as Text
+import Test.IDE.SchemaDesigner.ParserSpec (parseSqlStatements)
+
+tests = do
+    describe "SchemaCompiler" do
+        describe "compileEnumDataDefinitions" do
+            it "should deal with enum values that have spaces" do
+                let statement = CreateEnumType { name = "mood", values = ["happy", "very happy", "sad", "very sad"] }
+                let output = compileStatementPreview [statement] statement |> Text.strip
+
+                output `shouldBe` [trimming|
+                    data Mood = Happy | VeryHappy | Sad | VerySad deriving (Eq, Show, Read, Enum, Bounded, Ord)
+                    instance FromField Mood where
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "happy") = pure Happy
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "very happy") = pure VeryHappy
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "sad") = pure Sad
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "very sad") = pure VerySad
+                        fromField field (Just value) = returnError ConversionFailed field ("Unexpected value for enum value. Got: " <> Data.String.Conversions.cs value)
+                        fromField field Nothing = returnError UnexpectedNull field "Unexpected null for enum value"
+                    instance Default Mood where def = Happy
+                    instance ToField Mood where
+                        toField Happy = toField ("happy" :: Text)
+                        toField VeryHappy = toField ("very happy" :: Text)
+                        toField Sad = toField ("sad" :: Text)
+                        toField VerySad = toField ("very sad" :: Text)
+                    instance InputValue Mood where
+                        inputValue Happy = "happy" :: Text
+                        inputValue VeryHappy = "very happy" :: Text
+                        inputValue Sad = "sad" :: Text
+                        inputValue VerySad = "very sad" :: Text
+                    instance DeepSeq.NFData Mood where rnf a = seq a ()
+                    instance IHP.Controller.Param.ParamReader Mood where readParameter = IHP.Controller.Param.enumParamReader; readParameterJSON = IHP.Controller.Param.enumParamReaderJSON
+                    textToEnumMoodMap :: HashMap.HashMap Text Mood
+                    textToEnumMoodMap = HashMap.fromList [("happy", Happy), ("very happy", VeryHappy), ("sad", Sad), ("very sad", VerySad)]
+                    textToEnumMood :: Text -> Maybe Mood
+                    textToEnumMood t = HashMap.lookup t textToEnumMoodMap
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder Mood where
+                        defaultParam = Hasql.Encoders.nonNullable (Hasql.Encoders.enum (Just "public") "mood" inputValue)
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder (Maybe Mood) where
+                        defaultParam = Hasql.Encoders.nullable (Hasql.Encoders.enum (Just "public") "mood" inputValue)
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder [Mood] where
+                        defaultParam = Hasql.Encoders.nonNullable $ Hasql.Encoders.foldableArray $ Hasql.Encoders.nonNullable (Hasql.Encoders.enum (Just "public") "mood" inputValue)
+                    instance Mapping.IsScalar Mood where
+                        encoder = Hasql.Encoders.enum (Just "public") "mood" inputValue
+                        decoder = Hasql.Decoders.enum (Just "public") "mood" textToEnumMood
+                |]
+            it "should deal with enums that have no values" do
+                -- https://github.com/digitallyinduced/ihp/issues/1026
+                -- Empty enums typically happen when an enum was just created in the schema designer and no value has been added yet by the user
+                let statement = CreateEnumType { name = "mood", values = [] }
+                let output = compileStatementPreview [statement] statement |> Text.strip
+
+                -- We don't generate anything when no values are defined as there's nothing you could do with the enum yet
+                -- An empty data declaration is not really useful in this case
+                output `shouldBe` mempty
+            it "should not pluralize values" do
+                -- See https://github.com/digitallyinduced/ihp/issues/767
+                let statement = CreateEnumType { name = "Province", values = ["Alberta", "BritishColumbia", "Saskatchewan", "Manitoba", "Ontario", "Quebec", "NovaScotia", "NewBrunswick", "PrinceEdwardIsland", "NewfoundlandAndLabrador"] }
+                let output = compileStatementPreview [statement] statement |> Text.strip
+
+                output `shouldBe` [trimming|
+                    data Province = Alberta | Britishcolumbia | Saskatchewan | Manitoba | Ontario | Quebec | Novascotia | Newbrunswick | Princeedwardisland | Newfoundlandandlabrador deriving (Eq, Show, Read, Enum, Bounded, Ord)
+                    instance FromField Province where
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "Alberta") = pure Alberta
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "BritishColumbia") = pure Britishcolumbia
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "Saskatchewan") = pure Saskatchewan
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "Manitoba") = pure Manitoba
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "Ontario") = pure Ontario
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "Quebec") = pure Quebec
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "NovaScotia") = pure Novascotia
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "NewBrunswick") = pure Newbrunswick
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "PrinceEdwardIsland") = pure Princeedwardisland
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "NewfoundlandAndLabrador") = pure Newfoundlandandlabrador
+                        fromField field (Just value) = returnError ConversionFailed field ("Unexpected value for enum value. Got: " <> Data.String.Conversions.cs value)
+                        fromField field Nothing = returnError UnexpectedNull field "Unexpected null for enum value"
+                    instance Default Province where def = Alberta
+                    instance ToField Province where
+                        toField Alberta = toField ("Alberta" :: Text)
+                        toField Britishcolumbia = toField ("BritishColumbia" :: Text)
+                        toField Saskatchewan = toField ("Saskatchewan" :: Text)
+                        toField Manitoba = toField ("Manitoba" :: Text)
+                        toField Ontario = toField ("Ontario" :: Text)
+                        toField Quebec = toField ("Quebec" :: Text)
+                        toField Novascotia = toField ("NovaScotia" :: Text)
+                        toField Newbrunswick = toField ("NewBrunswick" :: Text)
+                        toField Princeedwardisland = toField ("PrinceEdwardIsland" :: Text)
+                        toField Newfoundlandandlabrador = toField ("NewfoundlandAndLabrador" :: Text)
+                    instance InputValue Province where
+                        inputValue Alberta = "Alberta" :: Text
+                        inputValue Britishcolumbia = "BritishColumbia" :: Text
+                        inputValue Saskatchewan = "Saskatchewan" :: Text
+                        inputValue Manitoba = "Manitoba" :: Text
+                        inputValue Ontario = "Ontario" :: Text
+                        inputValue Quebec = "Quebec" :: Text
+                        inputValue Novascotia = "NovaScotia" :: Text
+                        inputValue Newbrunswick = "NewBrunswick" :: Text
+                        inputValue Princeedwardisland = "PrinceEdwardIsland" :: Text
+                        inputValue Newfoundlandandlabrador = "NewfoundlandAndLabrador" :: Text
+                    instance DeepSeq.NFData Province where rnf a = seq a ()
+                    instance IHP.Controller.Param.ParamReader Province where readParameter = IHP.Controller.Param.enumParamReader; readParameterJSON = IHP.Controller.Param.enumParamReaderJSON
+                    textToEnumProvinceMap :: HashMap.HashMap Text Province
+                    textToEnumProvinceMap = HashMap.fromList [("Alberta", Alberta), ("BritishColumbia", Britishcolumbia), ("Saskatchewan", Saskatchewan), ("Manitoba", Manitoba), ("Ontario", Ontario), ("Quebec", Quebec), ("NovaScotia", Novascotia), ("NewBrunswick", Newbrunswick), ("PrinceEdwardIsland", Princeedwardisland), ("NewfoundlandAndLabrador", Newfoundlandandlabrador)]
+                    textToEnumProvince :: Text -> Maybe Province
+                    textToEnumProvince t = HashMap.lookup t textToEnumProvinceMap
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder Province where
+                        defaultParam = Hasql.Encoders.nonNullable (Hasql.Encoders.enum (Just "public") "province" inputValue)
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder (Maybe Province) where
+                        defaultParam = Hasql.Encoders.nullable (Hasql.Encoders.enum (Just "public") "province" inputValue)
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder [Province] where
+                        defaultParam = Hasql.Encoders.nonNullable $ Hasql.Encoders.foldableArray $ Hasql.Encoders.nonNullable (Hasql.Encoders.enum (Just "public") "province" inputValue)
+                    instance Mapping.IsScalar Province where
+                        encoder = Hasql.Encoders.enum (Just "public") "province" inputValue
+                        decoder = Hasql.Decoders.enum (Just "public") "province" textToEnumProvince
+                |]
+            it "should deal with duplicate enum values" do
+                let enum1 = CreateEnumType { name = "property_type", values = ["APARTMENT", "HOUSE"] }
+                let enum2 = CreateEnumType { name = "apartment_type", values = ["LOFT", "APARTMENT"] }
+                let output = compileStatementPreview [enum1, enum2] enum1 |> Text.strip
+
+                output `shouldBe` [trimming|
+                    data PropertyType = PropertyTypeApartment | House deriving (Eq, Show, Read, Enum, Bounded, Ord)
+                    instance FromField PropertyType where
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "APARTMENT") = pure PropertyTypeApartment
+                        fromField field (Just value) | value == (Data.Text.Encoding.encodeUtf8 "HOUSE") = pure House
+                        fromField field (Just value) = returnError ConversionFailed field ("Unexpected value for enum value. Got: " <> Data.String.Conversions.cs value)
+                        fromField field Nothing = returnError UnexpectedNull field "Unexpected null for enum value"
+                    instance Default PropertyType where def = PropertyTypeApartment
+                    instance ToField PropertyType where
+                        toField PropertyTypeApartment = toField ("APARTMENT" :: Text)
+                        toField House = toField ("HOUSE" :: Text)
+                    instance InputValue PropertyType where
+                        inputValue PropertyTypeApartment = "APARTMENT" :: Text
+                        inputValue House = "HOUSE" :: Text
+                    instance DeepSeq.NFData PropertyType where rnf a = seq a ()
+                    instance IHP.Controller.Param.ParamReader PropertyType where readParameter = IHP.Controller.Param.enumParamReader; readParameterJSON = IHP.Controller.Param.enumParamReaderJSON
+                    textToEnumPropertyTypeMap :: HashMap.HashMap Text PropertyType
+                    textToEnumPropertyTypeMap = HashMap.fromList [("APARTMENT", PropertyTypeApartment), ("HOUSE", House)]
+                    textToEnumPropertyType :: Text -> Maybe PropertyType
+                    textToEnumPropertyType t = HashMap.lookup t textToEnumPropertyTypeMap
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder PropertyType where
+                        defaultParam = Hasql.Encoders.nonNullable (Hasql.Encoders.enum (Just "public") "property_type" inputValue)
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder (Maybe PropertyType) where
+                        defaultParam = Hasql.Encoders.nullable (Hasql.Encoders.enum (Just "public") "property_type" inputValue)
+                    instance Hasql.Implicits.Encoders.DefaultParamEncoder [PropertyType] where
+                        defaultParam = Hasql.Encoders.nonNullable $ Hasql.Encoders.foldableArray $ Hasql.Encoders.nonNullable (Hasql.Encoders.enum (Just "public") "property_type" inputValue)
+                    instance Mapping.IsScalar PropertyType where
+                        encoder = Hasql.Encoders.enum (Just "public") "property_type" inputValue
+                        decoder = Hasql.Decoders.enum (Just "public") "property_type" textToEnumPropertyType
+                |]
+        describe "compileCreate" do
+            let statement = StatementCreateTable $ (table "users") {
+                    columns = [ col "id" PUUID ],
+                    primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                }
+            let compileOutput = compileStatementPreview [statement] statement |> Text.strip
+
+            it "should compile CanCreate instance with sqlQuery" $ \statement -> do
+                getInstanceDecl "CanCreate" compileOutput `shouldBe` [trimming|
+                    instance CanCreate Generated.ActualTypes.User where
+                        create = createUser
+                        createMany = createManyUser
+                        createRecordDiscardResult = createRecordDiscardResultUser
+                    |]
+            it "should compile CanUpdate instance with sqlQuery" $ \statement -> do
+                getInstanceDecl "CanUpdate" compileOutput `shouldBe` [trimming|
+                    instance CanUpdate Generated.ActualTypes.User where
+                        updateRecord = updateRecordUser
+                        updateRecordDiscardResult = updateRecordDiscardResultUser
+                    |]
+
+            it "should compile CanUpdate instance with an array type with an explicit cast" do
+                let statement = StatementCreateTable $ (table "users") {
+                    columns = [ (col "id" PUUID) { notNull = True, isUnique = True }, col "ids" (PArray PUUID)],
+                    primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                }
+                let compileOutput = compileStatementPreview [statement] statement |> Text.strip
+
+                getInstanceDecl "CanUpdate" compileOutput `shouldBe` [trimming|
+                    instance CanUpdate Generated.ActualTypes.User where
+                        updateRecord = updateRecordUser
+                        updateRecordDiscardResult = updateRecordDiscardResultUser
+                    |]
+            it "should deal with double default values" do
+                let statement = StatementCreateTable (table "users")
+                        { columns =
+                            [ (col "id" PUUID) { notNull = True, isUnique = True }
+                            , col "ids" (PArray PUUID)
+                            , (col "electricity_unit_price" PDouble) { defaultValue = Just (TypeCastExpression (DoubleExpression 0.17) PDouble), notNull = True }
+                            ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        }
+                let compileOutput = compileStatementPreview [statement] statement |> Text.strip
+
+                compileOutput `shouldBe` [trimming|
+                    data User' = User {id :: (Id' "users"), ids :: (Maybe [UUID]), electricityUnitPrice :: Double, meta :: MetaBag} deriving (Eq, Show)
+
+                    type instance PrimaryKey "users" = UUID
+
+                    type User = User'
+
+                    type instance GetTableName (User') = "users"
+                    type instance GetModelByTableName "users" = User
+
+                    instance Default (Id' "users") where def = Id def
+
+                    instance IHP.ModelSupport.Table (User') where
+                        tableName = "users"
+                        columnNames = ["id","ids","electricity_unit_price"]
+                        primaryKeyColumnNames = ["id"]
+
+
+                    instance InputValue Generated.ActualTypes.User where inputValue = IHP.ModelSupport.recordToInputValue
+
+                    instance FromRow Generated.ActualTypes.User where
+                        fromRow = do
+                            id <- field
+                            ids <- field
+                            electricityUnitPrice <- field
+                            let theRecord = Generated.ActualTypes.User id ids electricityUnitPrice def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+
+                    instance FromRowHasql Generated.ActualTypes.User where
+                        hasqlRowDecoder = Generated.Statements.RowDecoderUser.rowDecoder
+
+                    type instance GetModelName (User') = "User"
+
+                    instance CanCreate Generated.ActualTypes.User where
+                        create = createUser
+                        createMany = createManyUser
+                        createRecordDiscardResult = createRecordDiscardResultUser
+
+                    createUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO Generated.ActualTypes.User
+                    createUser model = do
+                        let pool = ?modelContext.hasqlPool
+                        let touched = model.meta.touchedFields
+                        sqlStatementHasql pool model (Generated.Statements.CreateUser.statement touched)
+
+                    createManyUser :: (?modelContext :: ModelContext) => [Generated.ActualTypes.User] -> IO [Generated.ActualTypes.User]
+                    createManyUser [] = pure []
+                    createManyUser models = do
+                        let pool = ?modelContext.hasqlPool
+                        sqlStatementHasql pool models (Generated.Statements.CreateManyUser.statement (List.length models))
+
+                    createRecordDiscardResultUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO ()
+                    createRecordDiscardResultUser model = do
+                        let pool = ?modelContext.hasqlPool
+                        let touched = model.meta.touchedFields
+                        sqlStatementHasql pool model (Generated.Statements.CreateUser.discardResultStatement touched)
+
+                    instance CanUpdate Generated.ActualTypes.User where
+                        updateRecord = updateRecordUser
+                        updateRecordDiscardResult = updateRecordDiscardResultUser
+
+                    updateRecordUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO Generated.ActualTypes.User
+                    updateRecordUser model = do
+                        let touched = model.meta.touchedFields
+                        if touched == 0 then pure model else do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdateUser.statement touched)
+
+                    updateRecordDiscardResultUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO ()
+                    updateRecordDiscardResultUser model = do
+                        let touched = model.meta.touchedFields
+                        unless (touched == 0) $ do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdateUser.discardResultStatement touched)
+
+                    instance Record Generated.ActualTypes.User where
+                        {-# INLINE newRecord #-}
+                        newRecord = Generated.ActualTypes.User def def 0.17  def
+
+
+                    instance QueryBuilder.FilterPrimaryKey "users" where
+                        filterWhereId id builder =
+                            builder |> QueryBuilder.filterWhere (#id, id)
+                        {-# INLINE filterWhereId #-}
+
+                    instance FieldBit "id" (User') where fieldBit = 1
+                    instance FieldBit "ids" (User') where fieldBit = 2
+                    instance FieldBit "electricityUnitPrice" (User') where fieldBit = 4
+                |]
+            it "should deal with integer default values for double columns" do
+                let statement = StatementCreateTable (table "users")
+                        { columns =
+                            [ (col "id" PUUID) { notNull = True, isUnique = True }
+                            , col "ids" (PArray PUUID)
+                            , (col "electricity_unit_price" PDouble) { defaultValue = Just (IntExpression 0), notNull = True }
+                            ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        }
+                let compileOutput = compileStatementPreview [statement] statement |> Text.strip
+
+                compileOutput `shouldBe` [trimming|
+                    data User' = User {id :: (Id' "users"), ids :: (Maybe [UUID]), electricityUnitPrice :: Double, meta :: MetaBag} deriving (Eq, Show)
+
+                    type instance PrimaryKey "users" = UUID
+
+                    type User = User'
+
+                    type instance GetTableName (User') = "users"
+                    type instance GetModelByTableName "users" = User
+
+                    instance Default (Id' "users") where def = Id def
+
+                    instance IHP.ModelSupport.Table (User') where
+                        tableName = "users"
+                        columnNames = ["id","ids","electricity_unit_price"]
+                        primaryKeyColumnNames = ["id"]
+
+
+                    instance InputValue Generated.ActualTypes.User where inputValue = IHP.ModelSupport.recordToInputValue
+
+                    instance FromRow Generated.ActualTypes.User where
+                        fromRow = do
+                            id <- field
+                            ids <- field
+                            electricityUnitPrice <- field
+                            let theRecord = Generated.ActualTypes.User id ids electricityUnitPrice def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+
+                    instance FromRowHasql Generated.ActualTypes.User where
+                        hasqlRowDecoder = Generated.Statements.RowDecoderUser.rowDecoder
+
+                    type instance GetModelName (User') = "User"
+
+                    instance CanCreate Generated.ActualTypes.User where
+                        create = createUser
+                        createMany = createManyUser
+                        createRecordDiscardResult = createRecordDiscardResultUser
+
+                    createUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO Generated.ActualTypes.User
+                    createUser model = do
+                        let pool = ?modelContext.hasqlPool
+                        let touched = model.meta.touchedFields
+                        sqlStatementHasql pool model (Generated.Statements.CreateUser.statement touched)
+
+                    createManyUser :: (?modelContext :: ModelContext) => [Generated.ActualTypes.User] -> IO [Generated.ActualTypes.User]
+                    createManyUser [] = pure []
+                    createManyUser models = do
+                        let pool = ?modelContext.hasqlPool
+                        sqlStatementHasql pool models (Generated.Statements.CreateManyUser.statement (List.length models))
+
+                    createRecordDiscardResultUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO ()
+                    createRecordDiscardResultUser model = do
+                        let pool = ?modelContext.hasqlPool
+                        let touched = model.meta.touchedFields
+                        sqlStatementHasql pool model (Generated.Statements.CreateUser.discardResultStatement touched)
+
+                    instance CanUpdate Generated.ActualTypes.User where
+                        updateRecord = updateRecordUser
+                        updateRecordDiscardResult = updateRecordDiscardResultUser
+
+                    updateRecordUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO Generated.ActualTypes.User
+                    updateRecordUser model = do
+                        let touched = model.meta.touchedFields
+                        if touched == 0 then pure model else do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdateUser.statement touched)
+
+                    updateRecordDiscardResultUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO ()
+                    updateRecordDiscardResultUser model = do
+                        let touched = model.meta.touchedFields
+                        unless (touched == 0) $ do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdateUser.discardResultStatement touched)
+
+                    instance Record Generated.ActualTypes.User where
+                        {-# INLINE newRecord #-}
+                        newRecord = Generated.ActualTypes.User def def 0  def
+
+
+                    instance QueryBuilder.FilterPrimaryKey "users" where
+                        filterWhereId id builder =
+                            builder |> QueryBuilder.filterWhere (#id, id)
+                        {-# INLINE filterWhereId #-}
+
+                    instance FieldBit "id" (User') where fieldBit = 1
+                    instance FieldBit "ids" (User') where fieldBit = 2
+                    instance FieldBit "electricityUnitPrice" (User') where fieldBit = 4
+                |]
+            it "should not touch GENERATED columns" do
+                let statement = StatementCreateTable (table "users")
+                        { columns =
+                            [ (col "id" PUUID) { notNull = True, isUnique = True }
+                            , (col "ts" PTSVector) { notNull = True, generator = Just (ColumnGenerator { generate = VarExpression "someResult", stored = False }) }
+                            ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        }
+                let compileOutput = compileStatementPreview [statement] statement |> Text.strip
+
+                compileOutput `shouldBe` [trimming|
+                    data User' = User {id :: (Id' "users"), ts :: (Maybe Tsvector), meta :: MetaBag} deriving (Eq, Show)
+
+                    type instance PrimaryKey "users" = UUID
+
+                    type User = User'
+
+                    type instance GetTableName (User') = "users"
+                    type instance GetModelByTableName "users" = User
+
+                    instance Default (Id' "users") where def = Id def
+
+                    instance IHP.ModelSupport.Table (User') where
+                        tableName = "users"
+                        columnNames = ["id","ts"]
+                        primaryKeyColumnNames = ["id"]
+
+
+                    instance InputValue Generated.ActualTypes.User where inputValue = IHP.ModelSupport.recordToInputValue
+
+                    instance FromRow Generated.ActualTypes.User where
+                        fromRow = do
+                            id <- field
+                            ts <- field
+                            let theRecord = Generated.ActualTypes.User id ts def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+
+                    instance FromRowHasql Generated.ActualTypes.User where
+                        hasqlRowDecoder = Generated.Statements.RowDecoderUser.rowDecoder
+
+                    type instance GetModelName (User') = "User"
+
+                    instance CanCreate Generated.ActualTypes.User where
+                        create = createUser
+                        createMany = createManyUser
+                        createRecordDiscardResult = createRecordDiscardResultUser
+
+                    createUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO Generated.ActualTypes.User
+                    createUser model = do
+                        let pool = ?modelContext.hasqlPool
+                        sqlStatementHasql pool model Generated.Statements.CreateUser.statement
+
+                    createManyUser :: (?modelContext :: ModelContext) => [Generated.ActualTypes.User] -> IO [Generated.ActualTypes.User]
+                    createManyUser [] = pure []
+                    createManyUser models = do
+                        let pool = ?modelContext.hasqlPool
+                        sqlStatementHasql pool models (Generated.Statements.CreateManyUser.statement (List.length models))
+
+                    createRecordDiscardResultUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO ()
+                    createRecordDiscardResultUser model = do
+                        let pool = ?modelContext.hasqlPool
+                        sqlStatementHasql pool model Generated.Statements.CreateUser.discardResultStatement
+
+                    instance CanUpdate Generated.ActualTypes.User where
+                        updateRecord = updateRecordUser
+                        updateRecordDiscardResult = updateRecordDiscardResultUser
+
+                    updateRecordUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO Generated.ActualTypes.User
+                    updateRecordUser model = do
+                        let touched = model.meta.touchedFields
+                        if touched == 0 then pure model else do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdateUser.statement touched)
+
+                    updateRecordDiscardResultUser :: (?modelContext :: ModelContext) => Generated.ActualTypes.User -> IO ()
+                    updateRecordDiscardResultUser model = do
+                        let touched = model.meta.touchedFields
+                        unless (touched == 0) $ do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdateUser.discardResultStatement touched)
+
+                    instance Record Generated.ActualTypes.User where
+                        {-# INLINE newRecord #-}
+                        newRecord = Generated.ActualTypes.User def def  def
+
+
+                    instance QueryBuilder.FilterPrimaryKey "users" where
+                        filterWhereId id builder =
+                            builder |> QueryBuilder.filterWhere (#id, id)
+                        {-# INLINE filterWhereId #-}
+
+                    instance FieldBit "id" (User') where fieldBit = 1
+                    instance FieldBit "ts" (User') where fieldBit = 2
+                |]
+            it "should handle tablets with generated columns" do
+                let statement = StatementCreateTable CreateTable
+                        { name = "posts"
+                        , columns =
+                            [ (col "id" PUUID) { notNull = True, isUnique = True }
+                            , (col "title" PText) { notNull = True }
+                            , (col "body" PText) { notNull = True }
+                            , (col "body_index_col" PTSVector) { generator = Just (ColumnGenerator { generate = CallExpression "to_tsvector" [TextExpression "english", CallExpression "coalesce" [VarExpression "body", TextExpression ""]], stored = True }) }
+                            ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        , constraints = []
+                        , unlogged = False
+                        , inherits = Nothing
+                        }
+                let compileOutput = compileStatementPreview [statement] statement |> Text.strip
+
+                -- The key point: RETURNING clause should include body_index_col even though it's generated
+                getInstanceDecl "CanCreate" compileOutput `shouldBe` [trimming|
+                    instance CanCreate Generated.ActualTypes.Post where
+                        create = createPost
+                        createMany = createManyPost
+                        createRecordDiscardResult = createRecordDiscardResultPost
+                    |]
+
+                getInstanceDecl "CanUpdate" compileOutput `shouldBe` [trimming|
+                    instance CanUpdate Generated.ActualTypes.Post where
+                        updateRecord = updateRecordPost
+                        updateRecordDiscardResult = updateRecordDiscardResultPost
+                    |]
+            it "should deal with multiple has many relationships to the same table" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE landing_pages (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL
+                    );
+                    CREATE TABLE paragraph_ctas (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        landing_page_id UUID NOT NULL,
+                        to_landing_page_id UUID NOT NULL
+                    );
+                    ALTER TABLE paragraph_ctas ADD CONSTRAINT paragraph_ctas_ref_landing_page_id FOREIGN KEY (landing_page_id) REFERENCES landing_pages (id) ON DELETE NO ACTION;
+                    ALTER TABLE paragraph_ctas ADD CONSTRAINT paragraph_ctas_ref_to_landing_page_id FOREIGN KEY (to_landing_page_id) REFERENCES landing_pages (id) ON DELETE NO ACTION;
+                |]
+                let
+                    isTargetTable :: Statement -> Bool
+                    isTargetTable (StatementCreateTable CreateTable { name }) = name == "landing_pages"
+                    isTargetTable otherwise = False
+                let (Just statement) = find isTargetTable statements
+                let compileOutput = compileStatementPreview statements statement |> Text.strip
+
+                compileOutput `shouldBe` [trimming|
+                    data LandingPage' paragraphCtasLandingPages paragraphCtasToLandingPages = LandingPage {id :: (Id' "landing_pages"), paragraphCtasLandingPages :: paragraphCtasLandingPages, paragraphCtasToLandingPages :: paragraphCtasToLandingPages, meta :: MetaBag} deriving (Eq, Show)
+
+                    type instance PrimaryKey "landing_pages" = UUID
+                    
+                    type LandingPage = LandingPage' (QueryBuilder.QueryBuilder "paragraph_ctas") (QueryBuilder.QueryBuilder "paragraph_ctas")
+
+                    type instance GetTableName (LandingPage' _ _) = "landing_pages"
+                    type instance GetModelByTableName "landing_pages" = LandingPage
+
+                    instance Default (Id' "landing_pages") where def = Id def
+
+                    instance IHP.ModelSupport.Table (LandingPage' paragraphCtasLandingPages paragraphCtasToLandingPages) where
+                        tableName = "landing_pages"
+                        columnNames = ["id"]
+                        primaryKeyColumnNames = ["id"]
+
+
+                    instance InputValue Generated.ActualTypes.LandingPage where inputValue = IHP.ModelSupport.recordToInputValue
+
+                    instance FromRow Generated.ActualTypes.LandingPage where
+                        fromRow = do
+                            id <- field
+                            let theRecord = Generated.ActualTypes.LandingPage id (QueryBuilder.filterWhere (#landingPageId, id) (QueryBuilder.query @ParagraphCta)) (QueryBuilder.filterWhere (#toLandingPageId, id) (QueryBuilder.query @ParagraphCta)) def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+
+                    instance FromRowHasql Generated.ActualTypes.LandingPage where
+                        hasqlRowDecoder = Generated.Statements.RowDecoderLandingPage.rowDecoder
+
+                    type instance GetModelName (LandingPage' _ _) = "LandingPage"
+
+                    instance CanCreate Generated.ActualTypes.LandingPage where
+                        create = createLandingPage
+                        createMany = createManyLandingPage
+                        createRecordDiscardResult = createRecordDiscardResultLandingPage
+
+                    createLandingPage :: (?modelContext :: ModelContext) => Generated.ActualTypes.LandingPage -> IO Generated.ActualTypes.LandingPage
+                    createLandingPage model = do
+                        let pool = ?modelContext.hasqlPool
+                        let touched = model.meta.touchedFields
+                        sqlStatementHasql pool model (Generated.Statements.CreateLandingPage.statement touched)
+
+                    createManyLandingPage :: (?modelContext :: ModelContext) => [Generated.ActualTypes.LandingPage] -> IO [Generated.ActualTypes.LandingPage]
+                    createManyLandingPage [] = pure []
+                    createManyLandingPage models = do
+                        let pool = ?modelContext.hasqlPool
+                        sqlStatementHasql pool models (Generated.Statements.CreateManyLandingPage.statement (List.length models))
+
+                    createRecordDiscardResultLandingPage :: (?modelContext :: ModelContext) => Generated.ActualTypes.LandingPage -> IO ()
+                    createRecordDiscardResultLandingPage model = do
+                        let pool = ?modelContext.hasqlPool
+                        let touched = model.meta.touchedFields
+                        sqlStatementHasql pool model (Generated.Statements.CreateLandingPage.discardResultStatement touched)
+
+                    instance CanUpdate Generated.ActualTypes.LandingPage where
+                        updateRecord = updateRecordLandingPage
+                        updateRecordDiscardResult = updateRecordDiscardResultLandingPage
+
+                    updateRecordLandingPage :: (?modelContext :: ModelContext) => Generated.ActualTypes.LandingPage -> IO Generated.ActualTypes.LandingPage
+                    updateRecordLandingPage model = do
+                        let touched = model.meta.touchedFields
+                        if touched == 0 then pure model else do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdateLandingPage.statement touched)
+
+                    updateRecordDiscardResultLandingPage :: (?modelContext :: ModelContext) => Generated.ActualTypes.LandingPage -> IO ()
+                    updateRecordDiscardResultLandingPage model = do
+                        let touched = model.meta.touchedFields
+                        unless (touched == 0) $ do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdateLandingPage.discardResultStatement touched)
+
+                    instance Record Generated.ActualTypes.LandingPage where
+                        {-# INLINE newRecord #-}
+                        newRecord = Generated.ActualTypes.LandingPage def def def def
+
+
+                    instance QueryBuilder.FilterPrimaryKey "landing_pages" where
+                        filterWhereId id builder =
+                            builder |> QueryBuilder.filterWhere (#id, id)
+                        {-# INLINE filterWhereId #-}
+
+                    instance FieldBit "id" (LandingPage' paragraphCtasLandingPages paragraphCtasToLandingPages) where fieldBit = 1
+                |]
+            it "should not use DEFAULT for array columns" do
+                let statement = StatementCreateTable (table "users")
+                        { columns =
+                            [ (col "id" PUUID) { notNull = True, isUnique = True }
+                            , (col "keywords" (PArray PText)) { defaultValue = Just (VarExpression "NULL") }
+                            ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        }
+                let compileOutput = compileStatementPreview [statement] statement |> Text.strip
+
+                getInstanceDecl "CanCreate" compileOutput `shouldBe` [trimming|
+                    instance CanCreate Generated.ActualTypes.User where
+                        create = createUser
+                        createMany = createManyUser
+                        createRecordDiscardResult = createRecordDiscardResultUser
+                    |]
+        describe "compileStatementPreview for table with arbitrarily named primary key" do
+            let statements = parseSqlStatements [trimming|
+                CREATE TABLE things (
+                    thing_arbitrary_ident UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL
+                );
+                CREATE TABLE others (
+                    other_arbitrary_ident UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                    thing_ref UUID NOT NULL
+                );
+                ALTER TABLE others ADD CONSTRAINT other_thing_refs FOREIGN KEY (thing_ref) REFERENCES things (thing_arbitrary_ident) ON DELETE NO ACTION;
+            |]
+            let
+                isTargetTable :: Statement -> Bool
+                isTargetTable (StatementCreateTable CreateTable { name }) = name == "things"
+                isTargetTable otherwise = False
+            let (Just statement) = find isTargetTable statements
+            let compileOutput = compileStatementPreview statements statement |> Text.strip
+        
+            it "should compile CanCreate instance with sqlQuery" $ \statement -> do
+                getInstanceDecl "CanCreate" compileOutput `shouldBe` [trimming|
+                    instance CanCreate Generated.ActualTypes.Thing where
+                        create = createThing
+                        createMany = createManyThing
+                        createRecordDiscardResult = createRecordDiscardResultThing
+                    |]
+            it "should compile CanUpdate instance with sqlQuery" $ \statement -> do
+                getInstanceDecl "CanUpdate" compileOutput `shouldBe` [trimming|
+                    instance CanUpdate Generated.ActualTypes.Thing where
+                        updateRecord = updateRecordThing
+                        updateRecordDiscardResult = updateRecordDiscardResultThing
+                    |]
+            it "should compile FromRow instance" $ \statement -> do
+                getInstanceDecl "FromRow" compileOutput `shouldBe` [trimming|
+                    instance FromRow Generated.ActualTypes.Thing where
+                        fromRow = do
+                            thingArbitraryIdent <- field
+                            let theRecord = Generated.ActualTypes.Thing thingArbitraryIdent (QueryBuilder.filterWhere (#thingRef, thingArbitraryIdent) (QueryBuilder.query @Other)) def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+                    |]
+            it "should compile Table instance" $ \statement -> do
+                getInstanceDecl "IHP.ModelSupport.Table" compileOutput `shouldBe` [trimming|
+                    instance IHP.ModelSupport.Table (Thing' others) where
+                        tableName = "things"
+                        columnNames = ["thing_arbitrary_ident"]
+                        primaryKeyColumnNames = ["thing_arbitrary_ident"]
+
+                    |]
+            it "should compile QueryBuilder.FilterPrimaryKey instance" $ \statement -> do
+                getInstanceDecl "QueryBuilder.FilterPrimaryKey" compileOutput `shouldBe` [trimming|
+                    instance QueryBuilder.FilterPrimaryKey "things" where
+                        filterWhereId thingArbitraryIdent builder =
+                            builder |> QueryBuilder.filterWhere (#thingArbitraryIdent, thingArbitraryIdent)
+                        {-# INLINE filterWhereId #-}
+                    |]
+        describe "compileStatementPreview for table with composite primary key" do
+            let statements = parseSqlStatements [trimming|
+                CREATE TABLE bits (
+                    bit_arbitrary_ident UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL
+                );
+                CREATE TABLE parts (
+                    part_arbitrary_ident UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL
+                );
+                CREATE TABLE bit_part_refs (
+                    bit_ref UUID NOT NULL,
+                    part_ref UUID NOT NULL,
+                    PRIMARY KEY(bit_ref, part_ref)
+                );
+                ALTER TABLE bit_part_refs ADD CONSTRAINT bit_part_bit_refs FOREIGN KEY (bit_ref) REFERENCES bits (bit_arbitrary_ident) ON DELETE NO ACTION;
+                ALTER TABLE bit_part_refs ADD CONSTRAINT bit_part_part_refs FOREIGN KEY (part_ref) REFERENCES parts (part_arbitrary_ident) ON DELETE NO ACTION;
+            |]
+            let
+                isNamedTable :: Text -> Statement -> Bool
+                isNamedTable targetName (StatementCreateTable CreateTable { name }) = name == targetName
+                isNamedTable _ _ = False
+            let (Just statement) = find (isNamedTable "bit_part_refs") statements
+            let compileOutput = compileStatementPreview statements statement |> Text.strip
+        
+            it "should compile CanCreate instance with sqlQuery" $ \statement -> do
+                getInstanceDecl "CanCreate" compileOutput `shouldBe` [trimming|
+                    instance CanCreate Generated.ActualTypes.BitPartRef where
+                        create = createBitPartRef
+                        createMany = createManyBitPartRef
+                        createRecordDiscardResult = createRecordDiscardResultBitPartRef
+                    |]
+            it "should compile CanUpdate instance with sqlQuery" $ \statement -> do
+                getInstanceDecl "CanUpdate" compileOutput `shouldBe` [trimming|
+                    instance CanUpdate Generated.ActualTypes.BitPartRef where
+                        updateRecord = updateRecordBitPartRef
+                        updateRecordDiscardResult = updateRecordDiscardResultBitPartRef
+                    |]
+            it "should compile FromRow instance" $ \statement -> do
+                getInstanceDecl "FromRow" compileOutput `shouldBe` [trimming|
+                    instance FromRow Generated.ActualTypes.BitPartRef where
+                        fromRow = do
+                            bitRef <- field
+                            partRef <- field
+                            let theRecord = Generated.ActualTypes.BitPartRef bitRef partRef def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+                    |]
+            it "should compile Table instance" $ \statement -> do
+                getInstanceDecl "IHP.ModelSupport.Table" compileOutput `shouldBe` [trimming|
+                    instance IHP.ModelSupport.Table (BitPartRef' bitRef partRef) where
+                        tableName = "bit_part_refs"
+                        columnNames = ["bit_ref","part_ref"]
+                        primaryKeyColumnNames = ["bit_ref","part_ref"]
+
+                    |]
+            it "should compile FromRow instance of table that references part of a composite key" $ \statement -> do
+                let (Just statement) = find (isNamedTable "parts") statements
+                let compileOutput = compileStatementPreview statements statement |> Text.strip
+                getInstanceDecl "FromRow" compileOutput `shouldBe` [trimming|
+                    instance FromRow Generated.ActualTypes.Part where
+                        fromRow = do
+                            partArbitraryIdent <- field
+                            let theRecord = Generated.ActualTypes.Part partArbitraryIdent (QueryBuilder.filterWhere (#partRef, partArbitraryIdent) (QueryBuilder.query @BitPartRef)) def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+                    |]
+            it "should compile QueryBuilder.FilterPrimaryKey instance" $ \statement -> do
+                getInstanceDecl "QueryBuilder.FilterPrimaryKey" compileOutput `shouldBe` [trimming|
+                    instance QueryBuilder.FilterPrimaryKey "bit_part_refs" where
+                        filterWhereId (Id (bitRef, partRef)) builder =
+                            builder |> QueryBuilder.filterWhere (#bitRef, bitRef) |> QueryBuilder.filterWhere (#partRef, partRef)
+                        {-# INLINE filterWhereId #-}
+                    |]
+        describe "compileFilterPrimaryKeyInstance" do
+            it "should compile FilterPrimaryKey instance when primary key is called id" do
+                let statement = StatementCreateTable $ (table "things") {
+                        columns = [ (col "id" PUUID) { notNull = True, isUnique = True } ],
+                        primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                    }
+                let compileOutput = compileStatementPreview [statement] statement |> Text.strip
+                
+                getInstanceDecl "QueryBuilder.FilterPrimaryKey" compileOutput `shouldBe` [trimming|
+                    instance QueryBuilder.FilterPrimaryKey "things" where
+                        filterWhereId id builder =
+                            builder |> QueryBuilder.filterWhere (#id, id)
+                        {-# INLINE filterWhereId #-}
+                    |]
+
+        describe "needsHasFieldId" do
+            let
+                isNamedTable :: Text -> Statement -> Bool
+                isNamedTable targetName (StatementCreateTable CreateTable { name }) = name == targetName
+                isNamedTable _ _ = False
+            it "should not generate HasField id for composite PK table with an id column" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE ideas_votes (
+                        id INT NOT NULL,
+                        idea_id UUID NOT NULL,
+                        parent_id UUID NOT NULL,
+                        PRIMARY KEY(idea_id, parent_id)
+                    );
+                |]
+                let (Just statement) = find (isNamedTable "ideas_votes") statements
+                let compileOutput = compileStatementPreview statements statement |> Text.strip
+
+                -- Should NOT contain a generated HasField "id" instance since the table has a column named "id"
+                compileOutput `shouldNotSatisfy` (Text.isInfixOf "instance HasField \"id\"")
+
+            it "should not generate HasField id for single non-id PK table with an id column" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE things (
+                        id INT NOT NULL,
+                        code TEXT PRIMARY KEY NOT NULL
+                    );
+                |]
+                let (Just statement) = find (isNamedTable "things") statements
+                let compileOutput = compileStatementPreview statements statement |> Text.strip
+
+                -- Should NOT contain a generated HasField "id" instance since the table has a column named "id"
+                compileOutput `shouldNotSatisfy` (Text.isInfixOf "instance HasField \"id\"")
+
+            it "should generate HasField id for composite PK table without an id column" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE bit_part_refs (
+                        bit_ref UUID NOT NULL,
+                        part_ref UUID NOT NULL,
+                        PRIMARY KEY(bit_ref, part_ref)
+                    );
+                |]
+                let (Just statement) = find (isNamedTable "bit_part_refs") statements
+                let compileOutput = compileStatementPreview statements statement |> Text.strip
+
+                -- Should contain a generated HasField "id" instance for the composite PK
+                compileOutput `shouldSatisfy` (Text.isInfixOf "instance HasField \"id\"")
+
+        describe "FK referencing non-PK column" do
+            it "should not generate type parameters or Include instances for non-PK FK columns" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        email TEXT NOT NULL UNIQUE
+                    );
+                    CREATE TABLE logins (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        user_email TEXT NOT NULL
+                    );
+                    ALTER TABLE logins ADD CONSTRAINT logins_ref_user_email FOREIGN KEY (user_email) REFERENCES users (email) ON DELETE NO ACTION;
+                |]
+                let
+                    isTargetTable :: Text -> Statement -> Bool
+                    isTargetTable targetName (StatementCreateTable CreateTable { name }) = name == targetName
+                    isTargetTable _ _ = False
+                let (Just loginStatement) = find (isTargetTable "logins") statements
+                let compileOutput = compileStatementPreview statements loginStatement |> Text.strip
+
+                -- userEmail should be Text (not Id' "users"), and no type parameter for it
+                compileOutput `shouldSatisfy` ("userEmail :: Text" `Text.isInfixOf`)
+                -- Should NOT have Include instance for userEmail (since it's not a PK-based FK)
+                compileOutput `shouldSatisfy` (not . ("Include \"userEmail\"" `Text.isInfixOf`))
+
+            it "should not generate has-many QueryBuilder field on the referenced table for non-PK FK" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        email TEXT NOT NULL UNIQUE
+                    );
+                    CREATE TABLE logins (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        user_email TEXT NOT NULL
+                    );
+                    ALTER TABLE logins ADD CONSTRAINT logins_ref_user_email FOREIGN KEY (user_email) REFERENCES users (email) ON DELETE NO ACTION;
+                |]
+                let
+                    isTargetTable :: Text -> Statement -> Bool
+                    isTargetTable targetName (StatementCreateTable CreateTable { name }) = name == targetName
+                    isTargetTable _ _ = False
+                let (Just userStatement) = find (isTargetTable "users") statements
+                let compileOutput = compileStatementPreview statements userStatement |> Text.strip
+
+                -- Users table should NOT have a has-many logins Include instance
+                compileOutput `shouldSatisfy` (not . ("Include \"logins\"" `Text.isInfixOf`))
+
+        describe "simple mode (compileRelationSupport = False)" do
+            let simpleOptions = previewCompilerOptions { compileRelationSupport = False }
+            it "should produce no type parameters and no QueryBuilder fields for a table with FK and has-many relations" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL
+                    );
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        title TEXT NOT NULL,
+                        user_id UUID NOT NULL
+                    );
+                    CREATE TABLE comments (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        post_id UUID NOT NULL,
+                        body TEXT NOT NULL
+                    );
+                    ALTER TABLE posts ADD CONSTRAINT posts_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                    ALTER TABLE comments ADD CONSTRAINT comments_ref_post_id FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE NO ACTION;
+                |]
+                let
+                    isTargetTable :: Text -> Statement -> Bool
+                    isTargetTable targetName (StatementCreateTable CreateTable { name }) = name == targetName
+                    isTargetTable _ _ = False
+                let (Just postStatement) = find (isTargetTable "posts") statements
+                let compileOutput = compileStatementPreviewWith simpleOptions statements postStatement |> Text.strip
+
+                -- data Post' has no type parameters, no QueryBuilder field, and userId has concrete type
+                compileOutput `shouldBe` [trimming|
+                    data Post' = Post {id :: (Id' "posts"), title :: Text, userId :: (Id' "users"), meta :: MetaBag} deriving (Eq, Show)
+
+                    type instance PrimaryKey "posts" = UUID
+
+                    type Post = Post'
+
+                    type instance GetTableName (Post') = "posts"
+                    type instance GetModelByTableName "posts" = Post
+
+                    instance Default (Id' "posts") where def = Id def
+
+                    instance IHP.ModelSupport.Table (Post') where
+                        tableName = "posts"
+                        columnNames = ["id","title","user_id"]
+                        primaryKeyColumnNames = ["id"]
+
+
+                    instance InputValue Generated.ActualTypes.Post where inputValue = IHP.ModelSupport.recordToInputValue
+
+                    instance FromRow Generated.ActualTypes.Post where
+                        fromRow = do
+                            id <- field
+                            title <- field
+                            userId <- field
+                            let theRecord = Generated.ActualTypes.Post id title userId def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+
+                    instance FromRowHasql Generated.ActualTypes.Post where
+                        hasqlRowDecoder = Generated.Statements.RowDecoderPost.rowDecoder
+
+                    type instance GetModelName (Post') = "Post"
+
+                    instance CanCreate Generated.ActualTypes.Post where
+                        create = createPost
+                        createMany = createManyPost
+                        createRecordDiscardResult = createRecordDiscardResultPost
+
+                    createPost :: (?modelContext :: ModelContext) => Generated.ActualTypes.Post -> IO Generated.ActualTypes.Post
+                    createPost model = do
+                        let pool = ?modelContext.hasqlPool
+                        let touched = model.meta.touchedFields
+                        sqlStatementHasql pool model (Generated.Statements.CreatePost.statement touched)
+
+                    createManyPost :: (?modelContext :: ModelContext) => [Generated.ActualTypes.Post] -> IO [Generated.ActualTypes.Post]
+                    createManyPost [] = pure []
+                    createManyPost models = do
+                        let pool = ?modelContext.hasqlPool
+                        sqlStatementHasql pool models (Generated.Statements.CreateManyPost.statement (List.length models))
+
+                    createRecordDiscardResultPost :: (?modelContext :: ModelContext) => Generated.ActualTypes.Post -> IO ()
+                    createRecordDiscardResultPost model = do
+                        let pool = ?modelContext.hasqlPool
+                        let touched = model.meta.touchedFields
+                        sqlStatementHasql pool model (Generated.Statements.CreatePost.discardResultStatement touched)
+
+                    instance CanUpdate Generated.ActualTypes.Post where
+                        updateRecord = updateRecordPost
+                        updateRecordDiscardResult = updateRecordDiscardResultPost
+
+                    updateRecordPost :: (?modelContext :: ModelContext) => Generated.ActualTypes.Post -> IO Generated.ActualTypes.Post
+                    updateRecordPost model = do
+                        let touched = model.meta.touchedFields
+                        if touched == 0 then pure model else do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdatePost.statement touched)
+
+                    updateRecordDiscardResultPost :: (?modelContext :: ModelContext) => Generated.ActualTypes.Post -> IO ()
+                    updateRecordDiscardResultPost model = do
+                        let touched = model.meta.touchedFields
+                        unless (touched == 0) $ do
+                            let pool = ?modelContext.hasqlPool
+                            sqlStatementHasql pool model (Generated.Statements.UpdatePost.discardResultStatement touched)
+
+                    instance Record Generated.ActualTypes.Post where
+                        {-# INLINE newRecord #-}
+                        newRecord = Generated.ActualTypes.Post def def def  def
+
+
+                    instance QueryBuilder.FilterPrimaryKey "posts" where
+                        filterWhereId id builder =
+                            builder |> QueryBuilder.filterWhere (#id, id)
+                        {-# INLINE filterWhereId #-}
+
+                    instance FieldBit "id" (Post') where fieldBit = 1
+                    instance FieldBit "title" (Post') where fieldBit = 2
+                    instance FieldBit "userId" (Post') where fieldBit = 4
+                |]
+            it "should produce no type parameters for a table that is referenced by other tables" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL
+                    );
+                    CREATE TABLE posts (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        user_id UUID NOT NULL
+                    );
+                    ALTER TABLE posts ADD CONSTRAINT posts_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
+                |]
+                let
+                    isTargetTable :: Text -> Statement -> Bool
+                    isTargetTable targetName (StatementCreateTable CreateTable { name }) = name == targetName
+                    isTargetTable _ _ = False
+                let (Just userStatement) = find (isTargetTable "users") statements
+                let compileOutput = compileStatementPreviewWith simpleOptions statements userStatement |> Text.strip
+
+                -- User has no has-many posts field, no type parameters
+                getInstanceDecl "Record" compileOutput `shouldBe` [trimming|
+                    instance Record Generated.ActualTypes.User where
+                        {-# INLINE newRecord #-}
+                        newRecord = Generated.ActualTypes.User def  def
+                |]
+
+            it "should use the referenced column's type when FK points to a non-PK column" do
+                let statements = parseSqlStatements [trimming|
+                    CREATE TABLE users (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        email TEXT NOT NULL UNIQUE
+                    );
+                    CREATE TABLE logins (
+                        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                        user_email TEXT NOT NULL
+                    );
+                    ALTER TABLE logins ADD CONSTRAINT logins_ref_user_email FOREIGN KEY (user_email) REFERENCES users (email) ON DELETE NO ACTION;
+                |]
+                let
+                    isTargetTable :: Text -> Statement -> Bool
+                    isTargetTable targetName (StatementCreateTable CreateTable { name }) = name == targetName
+                    isTargetTable _ _ = False
+                let (Just loginStatement) = find (isTargetTable "logins") statements
+                let compileOutput = compileStatementPreviewWith simpleOptions statements loginStatement |> Text.strip
+
+                -- userEmail should be Text, not Id' "users"
+                compileOutput `shouldSatisfy` ("userEmail :: Text" `Text.isInfixOf`)
+
+        describe "statement module content" do
+            let statements =
+                    [ StatementCreateTable CreateTable
+                        { name = "posts"
+                        , columns =
+                            [ (col "id" PUUID) { notNull = True, isUnique = True }
+                            , (col "title" PText) { notNull = True }
+                            , (col "body" PText) { notNull = True }
+                            ]
+                        , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                        , constraints = []
+                        , unlogged = False
+                        , inherits = Nothing
+                        }
+                    ]
+            let [StatementCreateTable theTable] = statements
+            let ?schema = Schema statements
+            let ?compilerOptions = fullCompileOptions
+
+            it "should generate correct RowDecoder statement module" do
+                let output = compileRowDecoderModule theTable
+                getStatementBody output `shouldBe` [trimming|
+                    rowDecoder :: Decoders.Row Generated.ActualTypes.Post
+                    rowDecoder = do
+                        id <- Decoders.column (Decoders.nonNullable Mapping.decoder)
+                        title <- Decoders.column (Decoders.nonNullable Decoders.text)
+                        body <- Decoders.column (Decoders.nonNullable Decoders.text)
+                        pure (let theRecord = Generated.ActualTypes.Post id title body def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) } in theRecord)
+                    |]
+
+            it "should generate nonNullable decoder for PRIMARY KEY column even without explicit NOT NULL (#2531)" do
+                let bugStatements =
+                        [ StatementCreateTable CreateTable
+                            { name = "bars"
+                            , columns =
+                                [ (col "id" PUUID) { defaultValue = Just (CallExpression "uuid_generate_v4" []) }
+                                , (col "ticker" PText) { notNull = True }
+                                , (col "date" PDate) { notNull = True }
+                                ]
+                            , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                            , constraints = []
+                            , unlogged = False
+                            , inherits = Nothing
+                            }
+                        ]
+                let [StatementCreateTable bugTable] = bugStatements
+                let ?schema = Schema bugStatements
+                let output = compileRowDecoderModule bugTable
+                getStatementBody output `shouldBe` [trimming|
+                    rowDecoder :: Decoders.Row Generated.ActualTypes.Bar
+                    rowDecoder = do
+                        id <- Decoders.column (Decoders.nonNullable Mapping.decoder)
+                        ticker <- Decoders.column (Decoders.nonNullable Decoders.text)
+                        date <- Decoders.column (Decoders.nonNullable Decoders.date)
+                        pure (let theRecord = Generated.ActualTypes.Bar id ticker date def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) } in theRecord)
+                    |]
+
+            it "should generate correct Create statement module" do
+                let output = compileCreateStatement theTable
+                getStatementBody output `shouldBe` [trimming|
+                    statement :: Statement.Statement Generated.ActualTypes.Post Generated.ActualTypes.Post
+                    statement = Statement.preparable sqlReturningResult encoder decoder
+
+                    discardResultStatement :: Statement.Statement Generated.ActualTypes.Post ()
+                    discardResultStatement = Statement.preparable sqlDiscardResult encoder Decoders.noResult
+
+                    sql :: Bool -> Text
+                    sql returning = "INSERT INTO posts (id, title, body) VALUES ($$1, $$2, $$3)"
+                        <> if returning then " RETURNING id, title, body" else ""
+
+                    sqlReturningResult :: Text
+                    sqlReturningResult = sql True
+
+                    sqlDiscardResult :: Text
+                    sqlDiscardResult = sql False
+
+                    encoder :: Encoders.Params Generated.ActualTypes.Post
+                    encoder =
+                            mconcat
+                                [ (.id) >$$< Encoders.param (Encoders.nonNullable Mapping.encoder)
+                                , (.title) >$$< Encoders.param (Encoders.nonNullable Encoders.text)
+                                , (.body) >$$< Encoders.param (Encoders.nonNullable Encoders.text)
+                                ]
+
+                    decoder :: Decoders.Result Generated.ActualTypes.Post
+                    decoder = Decoders.singleRow RowDecoder.rowDecoder
+                    |]
+
+            it "should generate correct Update statement module" do
+                let output = compileUpdateStatement theTable
+                getStatementBody output `shouldBe` [trimming|
+                    statement :: Integer -> Statement.Statement Generated.ActualTypes.Post Generated.ActualTypes.Post
+                    statement touchedFields = Statement.preparable (sql touchedFields True) (encoder touchedFields) decoder
+
+                    discardResultStatement :: Integer -> Statement.Statement Generated.ActualTypes.Post ()
+                    discardResultStatement touchedFields = Statement.preparable (sql touchedFields False) (encoder touchedFields) Decoders.noResult
+
+                    sql :: Integer -> Bool -> Text
+                    sql touchedFields returning =
+                        let setEntries = catMaybes
+                                [ if testBit touchedFields 1 then Just "title" else Nothing
+                                , if testBit touchedFields 2 then Just "body" else Nothing
+                                ]
+                            setClauses = [col <> " = $$" <> Text.pack (show i) | (i, col) <- zip [1..] setEntries]
+                            pkIdx = length setEntries + 1
+                            whereClause = \startIdx -> "id" <> " = $$" <> Text.pack (show startIdx)
+                            returningClause = if returning then " RETURNING id, title, body" else ""
+                        in "UPDATE posts SET " <> Text.intercalate ", " setClauses <> " WHERE " <> whereClause pkIdx <> returningClause
+
+
+                    encoder :: Integer -> Encoders.Params Generated.ActualTypes.Post
+                    encoder touchedFields = mconcat (catMaybes
+                        [ if testBit touchedFields 1 then Just ((.title) >$$< Encoders.param (Encoders.nonNullable Encoders.text)) else Nothing
+                        , if testBit touchedFields 2 then Just ((.body) >$$< Encoders.param (Encoders.nonNullable Encoders.text)) else Nothing
+                        ])
+                        <> ((.id) >$$< Encoders.param (Encoders.nonNullable Mapping.encoder))
+
+
+                    decoder :: Decoders.Result Generated.ActualTypes.Post
+                    decoder = Decoders.singleRow RowDecoder.rowDecoder
+                    |]
+
+            it "should generate correct FetchById statement module" do
+                let output = compileFetchByIdStatement theTable
+                getStatementBody output `shouldBe` [trimming|
+                    statement :: Statement.Statement (Id' "posts") (Maybe Generated.ActualTypes.Post)
+                    statement = Statement.preparable sql encoder decoder
+
+                    sql :: Text
+                    sql = "SELECT id, title, body FROM posts WHERE id = $$1 LIMIT 1"
+
+                    encoder :: Encoders.Params (Id' "posts")
+                    encoder = Encoders.param (Encoders.nonNullable Mapping.encoder)
+
+                    decoder :: Decoders.Result (Maybe Generated.ActualTypes.Post)
+                    decoder = Decoders.rowMaybe RowDecoder.rowDecoder
+                    |]
+
+            it "should generate correct CreateMany statement module" do
+                let output = compileCreateManyStatement theTable
+                getStatementBody output `shouldBe` [trimming|
+                    statement :: Int -> Statement.Statement [Generated.ActualTypes.Post] [Generated.ActualTypes.Post]
+                    statement count = Statement.unpreparable (sql count) (encoder count) decoder
+
+                    sql :: Int -> Text
+                    sql count = "INSERT INTO posts (id, title, body) VALUES "
+                        <> Text.intercalate ", " [valueGroup (i * 3) | i <- [0..count - 1]]
+                        <> " RETURNING id, title, body"
+                      where
+                        valueGroup offset = "(" <> Text.intercalate ", " ["$$" <> Text.pack (show (offset + j)) | j <- [1..3]] <> ")"
+
+                    encoder :: Int -> Encoders.Params [Generated.ActualTypes.Post]
+                    encoder count = mconcat [contramap (!! i) singleEncoder | i <- [0..count - 1]]
+
+                    singleEncoder :: Encoders.Params Generated.ActualTypes.Post
+                    singleEncoder =
+                            mconcat
+                                [ (.id) >$$< Encoders.param (Encoders.nonNullable Mapping.encoder)
+                                , (.title) >$$< Encoders.param (Encoders.nonNullable Encoders.text)
+                                , (.body) >$$< Encoders.param (Encoders.nonNullable Encoders.text)
+                                ]
+
+                    decoder :: Decoders.Result [Generated.ActualTypes.Post]
+                    decoder = Decoders.rowList RowDecoder.rowDecoder
+                    |]
+
+            it "should use correct bit indices for columns in Update" do
+                let snakeStatements =
+                        [ StatementCreateTable CreateTable
+                            { name = "blog_posts"
+                            , columns =
+                                [ (col "id" PUUID) { notNull = True, isUnique = True }
+                                , (col "post_title" PText) { notNull = True }
+                                ]
+                            , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                            , constraints = []
+                            , unlogged = False
+                            , inherits = Nothing
+                            }
+                        ]
+                let [StatementCreateTable snakeTable] = snakeStatements
+                let ?schema = Schema snakeStatements
+                let output = compileUpdateStatement snakeTable
+                -- post_title is at index 1 in the columns list, so testBit should use 1
+                output `shouldSatisfy` Text.isInfixOf "testBit touchedFields 1"
+
+            it "should generate correct dynamic Create statement module with DEFAULT columns" do
+                let defaultStatements =
+                        [ StatementCreateTable CreateTable
+                            { name = "posts"
+                            , columns =
+                                [ (col "id" PUUID) { notNull = True, isUnique = True, defaultValue = Just (CallExpression "uuid_generate_v4" []) }
+                                , (col "title" PText) { notNull = True }
+                                , (col "created_at" PTimestampWithTimezone) { notNull = True, defaultValue = Just (CallExpression "now" []) }
+                                ]
+                            , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
+                            , constraints = []
+                            , unlogged = False
+                            , inherits = Nothing
+                            }
+                        ]
+                let [StatementCreateTable defaultTable] = defaultStatements
+                let ?schema = Schema defaultStatements
+                let output = compileCreateStatement defaultTable
+                getStatementBody output `shouldBe` [trimming|
+                    statement :: Integer -> Statement.Statement Generated.ActualTypes.Post Generated.ActualTypes.Post
+                    statement touchedFields = Statement.preparable (sql touchedFields True) (encoder touchedFields) decoder
+
+                    discardResultStatement :: Integer -> Statement.Statement Generated.ActualTypes.Post ()
+                    discardResultStatement touchedFields = Statement.preparable (sql touchedFields False) (encoder touchedFields) Decoders.noResult
+
+                    sql :: Integer -> Bool -> Text
+                    sql touchedFields returning =
+                        let entries = catMaybes
+                                [ if testBit touchedFields 0 then Just "id" else Nothing
+                                , Just "title"
+                                , if testBit touchedFields 2 then Just "created_at" else Nothing
+                                ]
+                            columns = Text.intercalate ", " entries
+                            placeholders = Text.intercalate ", " ["$$" <> Text.pack (show i) | i <- [1 .. length entries]]
+                            returningClause = if returning then " RETURNING id, title, created_at" else ""
+                        in if null entries
+                            then "INSERT INTO posts DEFAULT VALUES" <> returningClause
+                            else "INSERT INTO posts (" <> columns <> ") VALUES (" <> placeholders <> ")" <> returningClause
+
+
+                    encoder :: Integer -> Encoders.Params Generated.ActualTypes.Post
+                    encoder touchedFields = mconcat $$ catMaybes
+                        [ if testBit touchedFields 0 then Just ((.id) >$$< Encoders.param (Encoders.nonNullable Mapping.encoder)) else Nothing
+                        , Just ((.title) >$$< Encoders.param (Encoders.nonNullable Encoders.text))
+                        , if testBit touchedFields 2 then Just ((.createdAt) >$$< Encoders.param (Encoders.nonNullable Encoders.timestamptz)) else Nothing
+                        ]
+
+
+                    decoder :: Decoders.Result Generated.ActualTypes.Post
+                    decoder = Decoders.singleRow RowDecoder.rowDecoder
+                    |]
+
+        describe "table inheritance (INHERITS)" do
+            let statements = parseSqlStatements [trimming|
+                CREATE TABLE posts (
+                    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                    title TEXT NOT NULL,
+                    body TEXT NOT NULL
+                );
+                CREATE TABLE post_revisions (
+                    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
+                    revision_content TEXT NOT NULL
+                ) INHERITS (posts);
+            |]
+            let
+                isNamedTable :: Text -> Statement -> Bool
+                isNamedTable targetName (StatementCreateTable CreateTable { name }) = name == targetName
+                isNamedTable _ _ = False
+            let (Just childStatement) = find (isNamedTable "post_revisions") statements
+            let compileOutput = compileStatementPreview statements childStatement |> Text.strip
+
+            it "should include inherited columns in FromRow instance" do
+                getInstanceDecl "FromRow" compileOutput `shouldBe` [trimming|
+                    instance FromRow Generated.ActualTypes.PostRevision where
+                        fromRow = do
+                            id <- field
+                            revisionContent <- field
+                            title <- field
+                            body <- field
+                            let theRecord = Generated.ActualTypes.PostRevision id revisionContent title body def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
+                            pure theRecord
+                    |]
+
+            it "should include inherited columns in Table instance" do
+                getInstanceDecl "IHP.ModelSupport.Table" compileOutput `shouldBe` [trimming|
+                    instance IHP.ModelSupport.Table (PostRevision') where
+                        tableName = "post_revisions"
+                        columnNames = ["id","revision_content","title","body"]
+                        primaryKeyColumnNames = ["id"]
+
+                    |]
+
+            it "should include inherited columns in Record instance" do
+                getInstanceDecl "Record" compileOutput `shouldBe` [trimming|
+                    instance Record Generated.ActualTypes.PostRevision where
+                        {-# INLINE newRecord #-}
+                        newRecord = Generated.ActualTypes.PostRevision def def def def  def
+                    |]
+
+-- | Extract the body of a statement module (everything after the import block)
+getStatementBody :: Text -> Text
+getStatementBody full =
+    Text.splitOn "\n" full
+        |> dropWhile (\line -> "import " `isPrefixOf` line || isEmpty line || "-- " `isPrefixOf` line || "{-#" `isPrefixOf` line || "module " `isPrefixOf` line)
+        |> Text.unlines
+        |> Text.strip
+
+getInstanceDecl :: Text -> Text -> Text
+getInstanceDecl instanceName full =
+    Text.splitOn "\n" full
+        |> findInstanceDecl
+        |> takeInstanceDecl
+        |> Text.unlines
+        |> Text.strip
+    where
+        findInstanceDecl (line:rest)
+            | ("instance " <> instanceName) `isPrefixOf` line = line : rest
+            | otherwise = findInstanceDecl rest
+        findInstanceDecl [] = error ("didn't find instance declaration of " <> instanceName)
+
+        takeInstanceDecl (line:rest)
+            | isEmpty line = []
+            | otherwise = line : takeInstanceDecl rest
+        takeInstanceDecl [] = [] -- EOF reached
diff --git a/Test/ServerSpec.hs b/Test/ServerSpec.hs
new file mode 100644
--- /dev/null
+++ b/Test/ServerSpec.hs
@@ -0,0 +1,89 @@
+module Test.ServerSpec where
+
+import IHP.Prelude
+import Test.Hspec
+import Network.Wai
+import Network.Wai.Test
+import Network.HTTP.Types
+
+import IHP.Static (staticRouteShortcut)
+import Network.Wai.Middleware.AssetPath (assetPathMiddleware, assetPath)
+
+-- | A simple WAI app that responds with the pathInfo and rawPathInfo it received
+echoApp :: Application
+echoApp request respond = respond $ responseLBS status200 []
+    (cs (tshow request.pathInfo <> "|" <> cs request.rawPathInfo :: Text))
+
+-- | A fallback app that always responds with "fallback"
+fallbackApp :: Application
+fallbackApp _request respond = respond $ responseLBS status200 [] "fallback"
+
+makeRequest :: ByteString -> [Text] -> Request
+makeRequest rawPath segments = defaultRequest
+    { rawPathInfo = rawPath
+    , pathInfo = segments
+    }
+
+tests :: Spec
+tests = do
+    describe "staticRouteShortcut" $ do
+        it "routes /static/* requests to the static app with prefix stripped" $ do
+            let app = staticRouteShortcut echoApp fallbackApp
+            let request = makeRequest "/static/app.css" ["static", "app.css"]
+            response <- runSession (request' request) app
+            let body = cs (simpleBody response) :: Text
+            body `shouldBe` "[\"app.css\"]|/app.css"
+
+        it "routes /static/ nested paths with prefix stripped" $ do
+            let app = staticRouteShortcut echoApp fallbackApp
+            let request = makeRequest "/static/vendor/bootstrap.css" ["static", "vendor", "bootstrap.css"]
+            response <- runSession (request' request) app
+            let body = cs (simpleBody response) :: Text
+            body `shouldBe` "[\"vendor\",\"bootstrap.css\"]|/vendor/bootstrap.css"
+
+        it "routes /static/ root to the static app with empty path" $ do
+            let app = staticRouteShortcut echoApp fallbackApp
+            let request = makeRequest "/static/" ["static", ""]
+            response <- runSession (request' request) app
+            let body = cs (simpleBody response) :: Text
+            body `shouldBe` "[\"\"]|/"
+
+        it "routes non-static requests to the fallback app" $ do
+            let app = staticRouteShortcut echoApp fallbackApp
+            let request = makeRequest "/Users" ["Users"]
+            response <- runSession (request' request) app
+            simpleBody response `shouldBe` "fallback"
+
+        it "routes root requests to the fallback app" $ do
+            let app = staticRouteShortcut echoApp fallbackApp
+            let request = makeRequest "/" [""]
+            response <- runSession (request' request) app
+            simpleBody response `shouldBe` "fallback"
+
+        it "does not match /staticx or similar prefixes" $ do
+            let app = staticRouteShortcut echoApp fallbackApp
+            let request = makeRequest "/staticx/app.css" ["staticx", "app.css"]
+            response <- runSession (request' request) app
+            simpleBody response `shouldBe` "fallback"
+
+    describe "assetPath with /static/ prefix" $ do
+        it "generates /static/-prefixed paths when middleware is applied" $ do
+            let middleware = assetPathMiddleware "abc123" Nothing
+            let innerApp req respond = do
+                    let result = assetPath req "/app.css"
+                    respond $ responseLBS status200 [] (cs result)
+            let app = middleware innerApp
+            response <- runSession (request' defaultRequest) app
+            cs (simpleBody response) `shouldBe` ("/static/app.css?v=abc123" :: String)
+
+        it "skips /static/ prefix when base URL is specified" $ do
+            let middleware = assetPathMiddleware "abc123" (Just "https://cdn.example.com")
+            let innerApp req respond = do
+                    let result = assetPath req "/app.js"
+                    respond $ responseLBS status200 [] (cs result)
+            let app = middleware innerApp
+            response <- runSession (request' defaultRequest) app
+            cs (simpleBody response) `shouldBe` ("https://cdn.example.com/app.js?v=abc123" :: String)
+
+request' :: Request -> Session SResponse
+request' = srequest . flip SRequest ""
diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,25 @@
+# Changelog for `ihp-ide`
+
+## v1.5.0
+
+- Add PostgreSQL table inheritance (`INHERITS`) support in Schema Designer
+- Add PostgreSQL 18 support with UUIDv7 as default UUID function
+- Support multiple trigger events (INSERT OR UPDATE, etc.)
+- Add `SECURITY DEFINER` support for SQL functions
+- Enable Hoogle by default with auto-start and IDE sidebar link
+- Allow deselecting actions in the controller generator
+- Add app selector to Generate Controller from schema designer
+- Improve IDE logs viewer with devenv service log tabs
+- Add persistent socket for seamless app restarts
+- Migrate IDE Data Controller from postgresql-simple to hasql
+- Migrate Bootstrap 4 to Bootstrap 5 (CSS classes, data attributes, jQuery 4.0)
+- Upgrade Bootstrap 5.2.1 to 5.3.8
+- Fix Schema Designer layout issues, column visibility, and context menu width
+- Fix IDE Data Editor foreign key dropdown, tooltip positioning, boolean display
+- Fix app crash detection with persistent socket
+- Fix lazy IO crash in FileWatcher
+- Fix devenv up Ctrl+C leaving GHCi/web server running as orphan processes
+- Remove compile-time `+RTS` flags (were incompatible with Hackage)
+- Replace snippet codegen with hasql statement modules
+- Improve code generators with type-aware scaffolding
+- Use `OsPath` instead of `FilePath`
diff --git a/data/lib/IHP/Makefile.dist b/data/lib/IHP/Makefile.dist
--- a/data/lib/IHP/Makefile.dist
+++ b/data/lib/IHP/Makefile.dist
@@ -7,6 +7,9 @@
 MAKEFLAGS += --warn-undefined-variables
 MAKEFLAGS += --no-builtin-rules
 
+PSQL_ARGS = "$$DATABASE_URL"
+PG_DUMP_ARGS = "$$DATABASE_URL"
+
 GHC_EXTENSIONS=
 GHC_EXTENSIONS+= -XGHC2021
 GHC_EXTENSIONS+= -XOverloadedStrings
@@ -96,46 +99,43 @@
 	build/bin/RunProdServer
 
 postgres: ## Starts the postgresql server
-	@if [ -s build/db/state/postmaster.pid ]; then echo "Postgres is already running."; else echo "Starting postgres"; postgres -D build/db/state -k $$PWD/build/db -c "listen_addresses="; fi
+	@echo "Postgres is managed by devenv. Use 'devenv up' to start all services including postgres."
 
 psql: ## Connects to the running postgresql server
-	@psql -h $$PWD/build/db -d app
+	@psql $(PSQL_ARGS)
 
 db: Application/Schema.sql Application/Fixtures.sql ## Creates a new database with the current Schema and imports Fixtures.sql
-	(echo "drop schema public cascade; create schema public;" | psql -h $${PWD}/build/db -d app) && \
-	psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < "$${IHP_LIB}/IHPSchema.sql" && \
-	psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < Application/Schema.sql && \
-	psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < Application/Fixtures.sql
+	(echo "drop schema public cascade; create schema public;" | psql $(PSQL_ARGS)) && \
+	psql -v ON_ERROR_STOP=1 $(PSQL_ARGS) < "$${IHP_LIB}/IHPSchema.sql" && \
+	psql -v ON_ERROR_STOP=1 $(PSQL_ARGS) < Application/Schema.sql && \
+	psql -v ON_ERROR_STOP=1 $(PSQL_ARGS) < Application/Fixtures.sql
 
 dumpdb: dump_db ## Saves the current database state into the Fixtures.sql
 dump_db: ## Saves the current database state into the Fixtures.sql
-	pg_dump -a --inserts --column-inserts --disable-triggers -h $$PWD/build/db app | sed -e '/^--/d' > Application/Fixtures.sql
+	pg_dump -a --inserts --column-inserts --disable-triggers $(PG_DUMP_ARGS) | sed -e '/^--/d' > Application/Fixtures.sql
 
 sql_dump:  ## Saves the current database state, so it can dumped into an SQL file `make sql_dump > dump.sql`
-	@pg_dump --no-owner --no-acl -h $$PWD/build/db app | sed -e '/^--/d'
+	@pg_dump --no-owner --no-acl $(PG_DUMP_ARGS) | sed -e '/^--/d'
 
 build/Generated/Types.hs: Application/Schema.sql ## Rebuilds generated types
 	mkdir -p build/Generated
 	build-generated-code
 
-build/bin/RunUnoptimizedProdServer: Main.hs build/bin static/prod.js static/prod.css build/Generated/Types.hs ## Quickly does a production build (all compiler optimizations disabled)
-	mkdir -p build/RunUnoptimizedProdServer
-	ghc -O0 ${GHC_OPTIONS} $< -o $@ -odir build/RunUnoptimizedProdServer -hidir build/RunUnoptimizedProdServer
-	chmod +x $<
-	rm -f build/bin/RunProdServer
-	ln -s `basename $@` build/bin/RunProdServer
+build/bin/RunUnoptimizedProdServer: ## Deprecated: Use 'nix build .#unoptimized-prod-server' instead
+	@echo "WARNING: This target is deprecated and may not work correctly."
+	@echo "Please use 'nix build .#unoptimized-prod-server' instead."
+	@echo "See https://ihp.digitallyinduced.com/Guide/deployment.html for more information."
+	@exit 1
 
-build/bin/RunOptimizedProdServer: Main.hs build/bin static/prod.js static/prod.css build/Generated/Types.hs ## Full production build with all ghc optimizations (takes a while)
-	mkdir -p build/RunOptimizedProdServer
-	ghc -O${OPTIMIZATION_LEVEL} ${GHC_OPTIONS} ${PROD_GHC_OPTIONS} $< -o $@ -odir build/RunOptimizedProdServer -hidir build/RunOptimizedProdServer
-	chmod +x $<
-	rm -f build/bin/RunProdServer
-	ln -s `basename $@` build/bin/RunProdServer
+build/bin/RunOptimizedProdServer: ## Deprecated: Use 'nix build .#optimized-prod-server' instead
+	@echo "WARNING: This target is deprecated and may not work correctly."
+	@echo "Please use 'nix build .#optimized-prod-server' instead."
+	@echo "See https://ihp.digitallyinduced.com/Guide/deployment.html for more information."
+	@exit 1
 
 clean: ## Resets all build and temporary files
 	rm -rf build/bin
 	rm -rf IHP/IHP/static/node_modules
-	rm -rf build/db
 
 static/prod.js: $(JS_FILES) ## Builds the production js bundle
 	awk -v RS='\0' '{print "(function (window, document, undefined) {"; print; print "\n})(window, document);";}' $(JS_FILES) > $@
@@ -152,15 +152,12 @@
 help: ## This help page
 	@grep -h -E '^[a-zA-Z_///-\.]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
 
-build/bin/Script/%: build/Script/Main/%.hs
-	mkdir -p build/bin/Script
-	# If RunOptimizedProdServer is compiled, likely we have object files with O2 in the build directory, so we're making an optimized script
-	# Otherwise we're doing a unoptimized compile as it's much faster
-	if [[ -f "build/bin/RunOptimizedProdServer" ]]; then \
-		ghc -O${OPTIMIZATION_LEVEL} ${GHC_OPTIONS} ${PROD_GHC_OPTIONS} $< -o $@ -odir build/RunOptimizedProdServer -hidir build/RunOptimizedProdServer; \
-	else \
-		ghc -O0 ${GHC_OPTIONS} $< -o $@ -odir build/RunUnoptimizedProdServer -hidir build/RunUnoptimizedProdServer; \
-	fi
+build/bin/Script/%: ## Deprecated: Use 'nix build' instead
+	@echo "WARNING: This target is deprecated and may not work correctly."
+	@echo "Please use 'nix build .#optimized-prod-server' or 'nix build .#unoptimized-prod-server' instead."
+	@echo "Script binaries will be available in result/bin/"
+	@echo "See https://ihp.digitallyinduced.com/Guide/deployment.html for more information."
+	@exit 1
 
 build/Script/Main/%.hs: Application/Script/%.hs
 	mkdir -p build/Script/Main
@@ -170,15 +167,19 @@
 	echo "import Application.Script.$* (run)" >> $@
 	echo "main = runScript Config.config run" >> $@
 
-build/bin/RunJobs: build/RunJobs.hs ## Builds an unoptimized binary for running the job workers
-	mkdir -p build/bin
-	ghc -O0 -main-is 'RunJobs.main' ${GHC_OPTIONS} $< -o $@ -odir build/RunUnoptimizedProdServer -hidir build/RunUnoptimizedProdServer # We use object files by build/bin/RunUnoptimizedProdServer here to speed up compilation during deployments
+build/bin/RunJobs: ## Deprecated: Use 'nix build' instead
+	@echo "WARNING: This target is deprecated and may not work correctly."
+	@echo "Please use 'nix build .#optimized-prod-server' or 'nix build .#unoptimized-prod-server' instead."
+	@echo "The RunJobs binary will be available in result/bin/RunJobs"
+	@echo "See https://ihp.digitallyinduced.com/Guide/deployment.html for more information."
+	@exit 1
 
-build/bin/RunJobsOptimized: build/RunJobs.hs ## Builds an optimized binary for running the job workers, symlinks it to build/bin/RunJobs
-	mkdir -p build/bin
-	ghc -O${OPTIMIZATION_LEVEL} -main-is 'RunJobs.main' ${GHC_OPTIONS} ${PROD_GHC_OPTIONS} $< -o $@ -odir build/RunOptimizedProdServer -hidir build/RunOptimizedProdServer # We use object files by build/bin/RunOptimizedProdServer here to speed up compilation during deployments
-	rm -f build/bin/RunJobs
-	ln -s `basename $@` build/bin/RunJobs
+build/bin/RunJobsOptimized: ## Deprecated: Use 'nix build .#optimized-prod-server' instead
+	@echo "WARNING: This target is deprecated and may not work correctly."
+	@echo "Please use 'nix build .#optimized-prod-server' instead."
+	@echo "The RunJobs binary will be available in result/bin/RunJobs"
+	@echo "See https://ihp.digitallyinduced.com/Guide/deployment.html for more information."
+	@exit 1
 
 build/RunJobs.hs: build/Generated/Types.hs
 	echo "module RunJobs (main) where" > $@
@@ -197,5 +198,9 @@
 # https://github.com/digitallyinduced/ihp/issues/776
 console: ghci ## Start IHP console, alias for 'make ghci'
 
-prepare-optimized-nix-build: ## Sets the 'enabled = true' flags in default.nix, so that a 'nix-build' makes optimized binaries
-	sed -i '' 's/haskellDeps/optimized = true;\n        haskellDeps/g' default.nix
+prepare-optimized-nix-build: ## Deprecated: Use 'nix build .#optimized-prod-server' instead
+	@echo "WARNING: This target is deprecated and no longer needed."
+	@echo "Please use 'nix build .#optimized-prod-server' for optimized builds."
+	@echo "Or use 'nix build .#unoptimized-prod-server' for faster, unoptimized builds."
+	@echo "See https://ihp.digitallyinduced.com/Guide/deployment.html for more information."
+	@exit 1
diff --git a/data/lib/IHP/applicationGhciConfig b/data/lib/IHP/applicationGhciConfig
--- a/data/lib/IHP/applicationGhciConfig
+++ b/data/lib/IHP/applicationGhciConfig
@@ -1,7 +1,6 @@
 :set prompt "\ESC[38;5;208m\STXIHP>\ESC[m\STX "
 :set -i.
 :set -iIHP/ihp-hsx
-:set -iIHP/ihp-postgresql-simple-extra
 :set -iConfig
 :set -ibuild
 :set -iIHP
@@ -41,6 +40,7 @@
 :set -XFunctionalDependencies
 :set -package ihp
 :set -fbyte-code
+:set -j
 :set -Wno-partial-type-signatures
 :set -XPartialTypeSignatures
 :set -XStandaloneDeriving
diff --git a/data/static/IDE/data-hovercard.js b/data/static/IDE/data-hovercard.js
--- a/data/static/IDE/data-hovercard.js
+++ b/data/static/IDE/data-hovercard.js
@@ -2,49 +2,52 @@
     initDataHoverCard();
 });
 
-function initDataHoverCard() {
-    $('td[data-foreign-key-column]').each(function () {
-        var element = this;
+// Dispose hovercards before Turbolinks replaces the page to prevent orphaned tooltip elements
+document.addEventListener('turbolinks:before-render', function () {
+    document.querySelectorAll('td[data-foreign-key-column]').forEach(function (el) {
+        var existing = bootstrap.Tooltip.getInstance(el);
+        if (existing) existing.dispose();
+    });
+});
 
-        $(element).tooltip({
+function initDataHoverCard() {
+    document.querySelectorAll('td[data-foreign-key-column]').forEach(function (element) {
+        var bsTooltip = new bootstrap.Tooltip(element, {
             title: "Loading",
             html: true,
             placement: 'left',
-            template: '<div class="tooltip foreign-key-hovercard" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
-            boundary: 'window'
+            container: 'body',
+            popperConfig: { strategy: 'fixed' },
+            template: '<div class="tooltip foreign-key-hovercard" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
         });
 
         var hoverCard = null;
 
         // When the `_id` column doesn't have a foreign key constraint
         // E.g. it's a stripe_customer_id column, then we get a 404 error
-        // by the server. In that case we just dispose the tooltip in `updateConte`
+        // by the server. In that case we just dispose the tooltip
         var isError = false;
 
         function updateContent() {
-            const tooltipId = element.getAttribute('aria-describedby');
-            const tooltipEl = document.getElementById(tooltipId);
+            var tooltipEl = bsTooltip.tip;
 
             if (!tooltipEl) {
                 return;
             }
             if (isError) {
-                $(element).tooltip('dispose');
+                bsTooltip.dispose();
                 return;
             }
-            const tooltipInner = tooltipEl.querySelector('.tooltip-inner');
+            var tooltipInner = tooltipEl.querySelector('.tooltip-inner');
 
             tooltipInner.innerHTML = hoverCard;
-
-            $(element).attr('title', hoverCard)
-            $(element).tooltip('update');
-            $(element).tooltip('_fixTitle');
+            bsTooltip.update();
         }
 
-        $(element).on('show.bs.tooltip', async function () {
+        element.addEventListener('show.bs.tooltip', async function () {
             var url = element.dataset.foreignKeyColumn;
             if (!hoverCard) {
-                hoverCard = fetch(url, { credentials: 'include' }).then(res => {
+                hoverCard = fetch(url, { credentials: 'include' }).then(function (res) {
                     if (!res.ok) {
                         console.log('ERROR');
                         isError = true;
@@ -57,10 +60,10 @@
                 updateContent();
             }
         });
-        $(element).on('shown.bs.tooltip', async function () {
-            if (!(hoverCard instanceof Promise) && !element.getAttribute('title')) {
+        element.addEventListener('shown.bs.tooltip', async function () {
+            if (typeof hoverCard === 'string') {
                 updateContent();
             }
-        })
-    })
+        });
+    });
 }
diff --git a/data/static/IDE/form.css b/data/static/IDE/form.css
--- a/data/static/IDE/form.css
+++ b/data/static/IDE/form.css
@@ -57,29 +57,22 @@
     background-color: hsl(192deg 100% 26% / 50%) !important;
 }
 
-.custom-control-label::before {
+.form-check-input {
     background-color: #002b36;
     border: 1px solid hsl(192deg 100% 30% / 50%);
 }
 
-.custom-checkbox .custom-control-input:checked~.custom-control-label::after {
-    top: 1px;
-}
-
-.custom-control-input:checked~.custom-control-label::before {
-    background-color:#519f98;
+.form-check-input:checked {
+    background-color: #519f98;
     border-color: #519f98;
 }
 
-.custom-control-input:focus~.custom-control-label::before {
+.form-check-input:focus {
     box-shadow: none !important;
-}
-
-.custom-control-input:focus:not(:checked)~.custom-control-label::before {
     border-color: #519f98;
 }
 
-.custom-control-label {
+.form-check-label {
     user-select: none;
     -webkit-user-select: none;
 }
diff --git a/data/static/IDE/ihp-help.js b/data/static/IDE/ihp-help.js
--- a/data/static/IDE/ihp-help.js
+++ b/data/static/IDE/ihp-help.js
@@ -1,11 +1,14 @@
-$(function () {
-    $('#nav-help').popover({
+document.addEventListener('turbolinks:load', function () {
+    var navHelp = document.getElementById('nav-help');
+    if (!navHelp) return;
+
+    new bootstrap.Popover(navHelp, {
         container: '#content',
         html: true,
         content: document.getElementById('help-content').innerHTML,
     });
 
-    document.getElementById('nav-help').addEventListener('click', event => {
+    navHelp.addEventListener('click', function (event) {
         event.preventDefault();
-    })
-})
+    });
+});
diff --git a/data/static/IDE/ihp-schemadesigner.js b/data/static/IDE/ihp-schemadesigner.js
--- a/data/static/IDE/ihp-schemadesigner.js
+++ b/data/static/IDE/ihp-schemadesigner.js
@@ -49,14 +49,18 @@
             $('#typeSelector').val('DATE').trigger('change');
         }
     });
-    $('.select2').select2({ tags: true });
-    $('.select2-simple').select2();
+    var $modal = $('.modal');
+    var select2Options = $modal.length ? { tags: true, dropdownParent: $modal } : { tags: true };
+    var select2SimpleOptions = $modal.length ? { dropdownParent: $modal } : {};
+    $('.select2').select2(select2Options);
+    $('.select2-simple').select2(select2SimpleOptions);
     $('#typeSelector').change(function () {
         switch (this.value) {
             case "UUID":
                 if ($('div[data-attribute="' + $("#colName").val() +'"]').length == 0) {
+                    var defaultUuidFn = document.body.getAttribute('data-default-uuid-function') || 'uuid_generate_v4()';
                     $('#defaultSelector').empty()
-                    .append(new Option("uuid_generate_v4()", 'uuid_generate_v4()', true, true))
+                    .append(new Option(defaultUuidFn, defaultUuidFn, true, true))
                     .append(new Option("no default", "", false, false))
                     .trigger('change');
                 } else {
@@ -186,7 +190,14 @@
 }
 
 function initTooltip() {
-    $('[data-toggle="tooltip"]').tooltip('dispose').tooltip({ container: 'body'});
+    // Remove any orphaned tooltip elements left over from Turbolinks/morphdom transitions
+    document.querySelectorAll('body > .tooltip').forEach(function (el) { el.remove(); });
+
+    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
+        var existing = bootstrap.Tooltip.getInstance(el);
+        if (existing) existing.dispose();
+        new bootstrap.Tooltip(el, { container: 'body', popperConfig: { strategy: 'fixed' } });
+    });
 }
 
 document.addEventListener('turbolinks:load', initSchemaDesigner);
@@ -195,6 +206,14 @@
 document.addEventListener('turbolinks:load', initTooltip);
 document.addEventListener('turbolinks:load', initDataEditorForeignKeyAutocomplete);
 
+// Dispose all tooltips before Turbolinks replaces the page to prevent orphaned tooltip elements
+document.addEventListener('turbolinks:before-render', function () {
+    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
+        var existing = bootstrap.Tooltip.getInstance(el);
+        if (existing) existing.dispose();
+    });
+});
+
 function initQueryAce() {
     var editorEl = document.getElementById('queryInput');
     if (!editorEl) return;
@@ -248,7 +267,7 @@
 function setSqlMode(id, sqlMode) {
     var inputField = document.getElementById(id + "-input");
     if (sqlMode) {
-        inputField.className = "form-control text-monospace text-secondary bg-light"
+        inputField.className = "form-control font-monospace text-secondary bg-light"
     } else {
         inputField.className = "form-control";
     }
@@ -269,7 +288,7 @@
         inputField.className = "d-none";
         inputField.name = id + "-inactive";
         hiddenField.name = id + "-inactive";
-        altField.className = "form-control text-monospace text-secondary bg-light";
+        altField.className = "form-control font-monospace text-secondary bg-light";
         altField.name = id;
         checkBoxContainer.className = "d-none";
         
@@ -336,18 +355,25 @@
 function initDataEditorForeignKeyAutocomplete() {
     const elements = document.querySelectorAll('.form-control.is-foreign-key-column');
 
+    var $modal = $('.modal');
     for (const element of elements) {
-        $(element).select2({
+        var options = {
             ajax: {
                 url: element.dataset.selectUrl,
-                processResults: data => ({ results: data }),
+                processResults: data => ({
+                    results: data.map(row => {
+                        row._originalKeys = Object.keys(row);
+                        return row;
+                    })
+                }),
                 cache: true
             },
             templateResult: row => {
                 const result = document.createElement('div');
                 result.classList.add('record');
 
-                for (const key in row) {
+                const keys = row._originalKeys || Object.keys(row);
+                for (const key of keys) {
                     const keyValueContainer = document.createElement('span');
                     keyValueContainer.classList.add('key-value-container');
 
@@ -378,6 +404,8 @@
                 return row.id;
             },
             placeholder:' Search ...'
-        })
+        };
+        if ($modal.length) options.dropdownParent = $modal;
+        $(element).select2(options);
     }
 }
diff --git a/data/static/IDE/ihp-toolserver-layout.css b/data/static/IDE/ihp-toolserver-layout.css
--- a/data/static/IDE/ihp-toolserver-layout.css
+++ b/data/static/IDE/ihp-toolserver-layout.css
@@ -184,6 +184,7 @@
 .context-menu-open {
     display: block;
     position: fixed;
+    width: fit-content;
 }
 
 /* slightly transparent fallback */
@@ -227,8 +228,16 @@
 .data-rows-table {
     font-size: 14px;
     width: 100%;
+    table-layout: fixed;
 }
 
+.data-rows-table td {
+    max-width: 300px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
 .data-rows-table [data-fieldname$="id"] { font-size: 8px }
 .data-rows-table [data-fieldname="created_at"] { font-size: 8px }
 .data-rows-table [data-fieldname="password_hash"] { font-size: 8px }
@@ -284,7 +293,7 @@
     color: #fdf6e3;
 }
 
-.border, .border-left, .border-bottom, .border-right, .border-top {
+.border, .border-start, .border-bottom, .border-end, .border-top {
     border-color: #0A4151 !important;
 }
 
@@ -296,7 +305,7 @@
     border-bottom: 1px solid #0A4151
 }
 
-.modal-header .close {
+.modal-header .btn-close {
     color: hsl(192deg 100% 30% / 50%);
     transition: all 0.2s;
 }
@@ -392,6 +401,6 @@
     background-color: hsl(193deg 80% 6%);
 }
 
-.bs-popover-auto[x-placement^=right]>.arrow::after, .bs-popover-right>.arrow::after {
+.bs-popover-auto[data-popper-placement^="right"]>.popover-arrow::after, .bs-popover-end>.popover-arrow::after {
     border-right-color: #0B5163;
 }
diff --git a/data/static/IDE/schema-designer.css b/data/static/IDE/schema-designer.css
--- a/data/static/IDE/schema-designer.css
+++ b/data/static/IDE/schema-designer.css
@@ -110,6 +110,7 @@
 }
 
 .column-selector .column td {
+	color: white;
     padding-top: 0px;
     padding-bottom: 0px;
     font-size: 10px;
@@ -150,7 +151,7 @@
 
 #editor {
     min-height: 800px;
-    background-color: #33555E#33555E
+    background-color: #33555E;
 }
 
 .row-form {
@@ -178,8 +179,9 @@
     margin-bottom: 200px; /* Make sure the context menu has a bit of space, see https://github.com/digitallyinduced/ihp/issues/895 */
 }
 
-.index .index-name, .policy .policy-name {
+.index .index-name, .policy .policy-name, .policy-expression {
     padding-left: 8px;
+    color: inherit;
 }
 
 .policy-expression small {
@@ -206,6 +208,11 @@
     background-color: #063642;
 }
 
+.column-selector .table {
+    --bs-table-bg: transparent;
+    --bs-table-hover-bg: hsla(192, 81%, 26%, 0.25);
+}
+
 .object {
     color: #E5EAEB;
     transition: all 0.1s;
@@ -239,6 +246,10 @@
     color:  #fff;
 }
 
+.column-selector a.text-body {
+    color: inherit !important;
+}
+
 .column-selector .column {
     color: #FFFFFF;
 }
@@ -252,12 +263,18 @@
 #schema-designer-viewer {
     margin-bottom: 0;
     flex-grow: 1;
+    flex-wrap: nowrap;
+    overflow: hidden;
 }
 
 .object-selector, .column-selector {
     min-height: 0 !important;
 }
 
+.object-selector {
+    min-width: 0;
+}
+
 .migration-status-visible .column-selector {
     padding-bottom: 100px;
 }
@@ -294,20 +311,20 @@
     font-size: 12px;
 }
 
-#column-options .custom-control {
+#column-options .form-check {
     display: flex;
     justify-content: center;
     align-items: center;
 }
 
-#column-options .custom-control-label::before {
-    top: 1px;
-}
-
 #editor .code-editor-container {
     min-height: 800px;
 }
 
+.modal-body {
+    overflow: visible;
+}
+
 .select2-results__option span.key {
     opacity: 0.5;
     margin-right: 12px;
@@ -332,26 +349,36 @@
 }
 
 #migration-status-container {
-    position: absolute;
-    bottom: 20px;
-    left: 0;
-    right: 0;
+    position: sticky;
+    bottom: 0;
+    z-index: 10;
+    width: 100%;
 }
 
 #migration-status-container > .alert {
+    --bs-alert-bg: #0A4151;
+    --bs-alert-color: hsla(196, 13%, 90%, 1);
+    --bs-alert-border-color: #0A4151;
+    --bs-alert-padding-x: 16px;
+    --bs-alert-padding-y: 8px;
+    --bs-alert-margin-bottom: 0;
     max-width: 800px;
     margin-left: auto;
     margin-right: auto;
+    margin-bottom: 0;
     font-size: 14px;
-    padding-top: 8px;
-    padding-bottom: 8px;
-    padding-left: 16px !important;
-    padding-right: 16px !important;
-    border: 2px solid #0B5163;
+    padding: 8px 16px;
+    border: 1px solid hsla(192, 81%, 26%, 0.4);
+    border-radius: 6px;
     background-color: #0A4151;
+    color: hsla(196, 13%, 90%, 1);
 }
 
 #migration-status-container .btn.btn-dark {
+    --bs-btn-bg: #002B37;
+    --bs-btn-border-color: hsla(192, 81%, 26%, 0.8);
+    --bs-btn-hover-bg: #003a4a;
+    --bs-btn-hover-border-color: hsla(192, 81%, 26%, 1);
     background-color: #002B37;
     border: 1px solid hsla(192, 81%, 26%, 0.8);
 }
diff --git a/data/static/IDE/table.css b/data/static/IDE/table.css
--- a/data/static/IDE/table.css
+++ b/data/static/IDE/table.css
@@ -9,7 +9,19 @@
 }
 
 .table {
-    color:  #fff;
+    --bs-table-bg: transparent;
+    --bs-table-color: #fff;
+    --bs-table-hover-bg: hsla(192, 81%, 26%, 0.25);
+    color: #fff;
+}
+
+/* Override Bootstrap's .table>:not(caption)>*>* which resets color on cells */
+.table > :not(caption) > * > * {
+    color: inherit;
+}
+
+.table td, .table th {
+    border-bottom: 0;
 }
 
 .table td a {
diff --git a/data/static/IDE/toolserver-logs.css b/data/static/IDE/toolserver-logs.css
--- a/data/static/IDE/toolserver-logs.css
+++ b/data/static/IDE/toolserver-logs.css
@@ -1,41 +1,62 @@
 #logs {
-    background-color: hsla(192, 81%, 3%, 1);
+    background-color: #0a1628;
     height: 100%;
     display: flex;
+    flex-direction: column;
+}
+
+#logs .logs-tab-bar {
+    display: flex;
     flex-direction: row;
+    gap: 0;
+    padding: 0 1rem;
+    background-color: #0d1b30;
+    border-bottom: 1px solid #1e2d45;
+    flex-shrink: 0;
 }
 
-#logs pre {
-    color: hsla(196, 13%, 90%, 1);
-    padding-left: 2rem;
-    padding-right: 4rem;
-    padding-top: 80px;
-    padding-bottom: 8rem;
-    margin-bottom: 0;
-    font-size: 11px;
+#logs .logs-tab {
+    padding: 0.6rem 1rem;
+    color: #8899aa;
+    font-weight: 500;
+    font-size: 12px;
+    text-decoration: none;
+    border-bottom: 2px solid transparent;
+    transition: color 0.15s, border-color 0.15s;
 }
 
-#logs pre:empty {
-    display: none;
+#logs .logs-tab:hover {
+    color: #c8d6e5;
 }
 
-#logs .logs-navigation {
-    height: 100%;
-    display: flex;
-    flex-direction: column;;
-    padding-top: 80px;
+#logs .logs-tab.active {
+    color: #e8f0fe;
+    border-bottom-color: #4d9fff;
 }
 
-#logs .logs-navigation a {
-    padding-left: 2rem;
-    padding-right: 1rem;
-    padding-bottom: 1rem;
-    color: #fdf6e3;
-    font-weight: 400;
-    font-size: 11px;
-    opacity: 0.5;
+#logs .logs-output {
+    flex: 1;
+    overflow-y: auto;
+    padding: 1rem 2rem 4rem;
 }
 
-#logs .logs-navigation a.active {
-    opacity: 1;
+#logs .logs-output pre {
+    color: #c8d6e5;
+    margin-bottom: 0;
+    font-family: 'SF Mono', Menlo, Monaco, 'Cascadia Code', monospace;
+    font-size: 12px;
+    line-height: 1.5;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    background: transparent;
+    border: none;
+    padding: 0;
+}
+
+#logs .logs-output pre:empty {
+    display: none;
+}
+
+#logs .logs-output pre.stderr {
+    color: #f0883e;
 }
diff --git a/data/static/IDE/tooltip.css b/data/static/IDE/tooltip.css
--- a/data/static/IDE/tooltip.css
+++ b/data/static/IDE/tooltip.css
@@ -12,7 +12,7 @@
 
     box-shadow: 0 1rem 3rem rgba(0,0,0,.175)!important;
 }
-.tooltip .arrow {
+.tooltip .tooltip-arrow {
     display: none;
 }
 
diff --git a/exe/IHP/CLI/BuildGeneratedCode.hs b/exe/IHP/CLI/BuildGeneratedCode.hs
deleted file mode 100644
--- a/exe/IHP/CLI/BuildGeneratedCode.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-{-|
-Module: IHP.CLI.BuildGeneratedCode
-Description:  Provides the @build-generated-code@ command which generates the Generated.Types module
-Copyright: (c) digitally induced GmbH, 2020
--}
-module Main where
-
-import IHP.Prelude
-import IHP.SchemaCompiler
-import Main.Utf8 (withUtf8)
-
-main :: IO ()
-main = withUtf8 do
-    compile
diff --git a/exe/IHP/CLI/DeleteController.hs b/exe/IHP/CLI/DeleteController.hs
--- a/exe/IHP/CLI/DeleteController.hs
+++ b/exe/IHP/CLI/DeleteController.hs
@@ -1,7 +1,7 @@
 module Main where
 
 import IHP.Prelude
-import qualified System.Directory as Directory
+import qualified System.Directory.OsPath as Directory
 import qualified System.Posix.Env.ByteString as Posix
 import IHP.IDE.CodeGen.ControllerGenerator
 import IHP.IDE.CodeGen.Controller (undoPlan)
@@ -22,8 +22,7 @@
         _ -> usage
 
 deleteController applicationName controllerName = do
-    let paginationEnabled = False
-    planOrError <- buildPlan applicationName controllerName paginationEnabled
+    planOrError <- buildPlan controllerName defaultControllerConfig { applicationName }
     case planOrError of
         Left error -> putStrLn error
         Right plan -> undoPlan $ reverse plan
diff --git a/exe/IHP/CLI/NewApplication.hs b/exe/IHP/CLI/NewApplication.hs
--- a/exe/IHP/CLI/NewApplication.hs
+++ b/exe/IHP/CLI/NewApplication.hs
@@ -1,7 +1,7 @@
 module Main where
 
 import IHP.Prelude
-import qualified System.Directory as Directory
+import qualified System.Directory.OsPath as Directory
 import qualified System.Posix.Env.ByteString as Posix
 import IHP.IDE.CodeGen.ApplicationGenerator
 import IHP.IDE.CodeGen.Controller (executePlan)
diff --git a/exe/IHP/CLI/NewController.hs b/exe/IHP/CLI/NewController.hs
--- a/exe/IHP/CLI/NewController.hs
+++ b/exe/IHP/CLI/NewController.hs
@@ -1,7 +1,7 @@
 module Main where
 
 import IHP.Prelude
-import qualified System.Directory as Directory
+import qualified System.Directory.OsPath as Directory
 import qualified System.Posix.Env.ByteString as Posix
 import IHP.IDE.CodeGen.ControllerGenerator
 import IHP.IDE.CodeGen.Controller (executePlan)
@@ -17,6 +17,7 @@
         Just "" -> usage
         Just appAndControllerName -> do
             generateController appAndControllerName
+        Nothing -> usage
 
 usage :: IO ()
 usage = putStrLn "Usage: new-controller RESOURCE_NAME"
@@ -29,15 +30,14 @@
     
 generateController :: Text -> IO ()
 generateController appAndControllerName = do
-    let paginationEnabled = False
     case Text.splitOn "." appAndControllerName of
         [controllerName] -> do
-            planOrError <- buildPlan controllerName "Web" paginationEnabled
+            planOrError <- buildPlan controllerName defaultControllerConfig
             case planOrError of
                 Left error -> putStrLn error
                 Right plan -> executePlan plan
         [applicationName, controllerName] -> do
-            planOrError <- buildPlan controllerName applicationName paginationEnabled
+            planOrError <- buildPlan controllerName defaultControllerConfig { applicationName }
             case planOrError of
                 Left error -> putStrLn error
                 Right plan -> executePlan plan
diff --git a/exe/IHP/CLI/NewMigration.hs b/exe/IHP/CLI/NewMigration.hs
--- a/exe/IHP/CLI/NewMigration.hs
+++ b/exe/IHP/CLI/NewMigration.hs
@@ -2,7 +2,7 @@
 
 import IHP.Prelude
 import qualified System.Posix.Env.ByteString as Posix
-import qualified System.Directory as Directory
+import qualified System.Directory.OsPath as Directory
 import IHP.IDE.ToolServer.Helper.Controller (openEditor)
 import qualified IHP.IDE.CodeGen.MigrationGenerator as MigrationGenerator
 import IHP.IDE.CodeGen.Controller (executePlan)
diff --git a/exe/IHP/IDE/DevServer.hs b/exe/IHP/IDE/DevServer.hs
--- a/exe/IHP/IDE/DevServer.hs
+++ b/exe/IHP/IDE/DevServer.hs
@@ -4,10 +4,7 @@
 import qualified System.Process as Process
 import IHP.HaskellSupport
 import qualified Data.ByteString.Char8 as ByteString
-import Control.Concurrent (myThreadId, threadDelay)
-import System.Exit
-import System.Posix.Signals
-
+import Control.Concurrent (threadDelay)
 import IHP.IDE.Types
 import IHP.IDE.Postgres
 import IHP.IDE.StatusServer
@@ -30,28 +27,34 @@
 import qualified Control.Concurrent.Chan.Unagi as Queue
 import IHP.IDE.FileWatcher
 import qualified System.Environment as Env
-import qualified System.Directory as Directory
+import qualified System.Directory.OsPath as Directory
 import qualified Control.Exception.Safe as Exception
 import qualified Data.ByteString.Builder as ByteString
 import qualified Network.Socket as Socket
+import qualified System.IO as IO
+import System.OsPath (OsPath, encodeUtf, decodeUtf)
 
+
 mainInParentDirectory :: IO ()
 mainInParentDirectory = do
     cwd <- Directory.getCurrentDirectory
-    mainInProjectDirectory (cwd <> "/../")
+    cwdStr <- decodeUtf cwd
+    projectDir <- encodeUtf (cwdStr <> "/../")
+    mainInProjectDirectory projectDir
 
-mainInProjectDirectory :: FilePath -> IO ()
+mainInProjectDirectory :: OsPath -> IO ()
 mainInProjectDirectory projectDir = do
     cwd <- Directory.getCurrentDirectory
+    cwdStr <- decodeUtf cwd
 
     withCurrentWorkingDirectory projectDir do
-        Env.setEnv "IHP_LIB" (cwd <> "/ihp-ide/lib/IHP")
-        Env.setEnv "TOOLSERVER_STATIC" (cwd <> "/ihp-ide/lib/IHP/static")
-        Env.setEnv "IHP_STATIC" (cwd <> "/lib/IHP/static")
+        Env.setEnv "IHP_LIB" (cwdStr <> "/ihp-ide/lib/IHP")
+        Env.setEnv "TOOLSERVER_STATIC" (cwdStr <> "/ihp-ide/lib/IHP/static")
+        Env.setEnv "IHP_STATIC" (cwdStr <> "/lib/IHP/static")
 
         mainWithOptions True
 
-withCurrentWorkingDirectory :: FilePath -> IO result -> IO result
+withCurrentWorkingDirectory :: OsPath -> IO result -> IO result
 withCurrentWorkingDirectory workingDirectory callback = do
     cwd <- Directory.getCurrentDirectory
     Exception.bracket_
@@ -64,19 +67,29 @@
 
 mainWithOptions :: Bool -> IO ()
 mainWithOptions wrapWithDirenv = withUtf8 do
+    -- https://github.com/digitallyinduced/ihp/issues/2134
+    -- devenv will redirect the standard handles to a pipe, causing block buffering by default
+    -- We need to override this so that `putStrLn` etc. works as expected
+    IO.hSetBuffering IO.stdout IO.LineBuffering
+    IO.hSetBuffering IO.stderr IO.LineBuffering
+
     databaseNeedsMigration <- newIORef False
     portConfig <- findAvailablePortConfig
-    ensureUserIsNotRoot
 
     -- Start the dev server in Debug mode by setting the env var DEBUG=1
     -- Like: $ DEBUG=1 devenv up
     isDebugMode <- EnvVar.envOrDefault "DEBUG" False
 
+    -- Create a persistent listening socket for the app port
+    -- This socket is shared between the status server and the app,
+    -- ensuring seamless transitions during app restarts (no connection refused errors)
+    appSocket <- createListeningSocket portConfig.appPort
+
     bracket (Log.newLogger def) (\logger -> logger.cleanup) \logger -> do
         (ghciInChan, ghciOutChan) <- Queue.newChan
         liveReloadClients <- newIORef mempty
         lastSchemaCompilerError <- newIORef Nothing
-        let ?context = Context { portConfig, isDebugMode, logger, ghciInChan, ghciOutChan, wrapWithDirenv, liveReloadClients, lastSchemaCompilerError }
+        let ?context = Context { portConfig, isDebugMode, logger, ghciInChan, ghciOutChan, wrapWithDirenv, liveReloadClients, lastSchemaCompilerError, appSocket }
 
         -- Print IHP Version when in debug mode
         when isDebugMode (Log.debug ("IHP Version: " <> Version.ihpVersion))
@@ -84,41 +97,37 @@
         ghciIsLoadingVar <- newIORef False
         reloadGhciVar :: MVar () <- newEmptyMVar
 
-        withBuiltinOrDevenvPostgres \databaseIsReady postgresStandardOutput postgresErrorOutput -> do
-            withStatusServer ghciIsLoadingVar \startStatusServer stopStatusServer statusServerStandardOutput statusServerErrorOutput statusServerClients -> do
-                -- Compile Schema before loading the app
-                tryCompileSchema reloadGhciVar startStatusServer
-                
-                let toolServerApplication = ToolServerApplication
-                        { postgresStandardOutput
-                        , postgresErrorOutput
-                        , appStandardOutput = statusServerStandardOutput
-                        , appErrorOutput = statusServerErrorOutput
-                        , appPort = portConfig.appPort
-                        , databaseNeedsMigration
-                        }
+        withStatusServer ghciIsLoadingVar \startStatusServer stopStatusServer statusServerStandardOutput statusServerErrorOutput statusServerClients -> do
+            -- Compile Schema before loading the app
+            tryCompileSchema reloadGhciVar startStatusServer
 
+            let toolServerApplication = ToolServerApplication
+                    { appStandardOutput = statusServerStandardOutput
+                    , appErrorOutput = statusServerErrorOutput
+                    , appPort = portConfig.appPort
+                    , databaseNeedsMigration
+                    }
 
-                void $ runConcurrently $ (,,,,,,)
-                        <$> Concurrently (updateDatabaseIsOutdated databaseNeedsMigration databaseIsReady)
-                        <*> Concurrently (runToolServer toolServerApplication liveReloadClients)
-                        <*> Concurrently (consumeGhciOutput statusServerStandardOutput statusServerErrorOutput statusServerClients)
-                        <*> Concurrently Telemetry.reportTelemetry
-                        <*> Concurrently (runFileWatcherWithDebounce (fileWatcherParams liveReloadClients databaseNeedsMigration databaseIsReady reloadGhciVar startStatusServer))
-                        <*> Concurrently (runAppGhci ghciIsLoadingVar startStatusServer stopStatusServer statusServerStandardOutput statusServerErrorOutput statusServerClients reloadGhciVar)
 
-            pure ()
+            void $ runConcurrently $ (,,,,,)
+                    <$> Concurrently (updateDatabaseIsOutdated databaseNeedsMigration)
+                    <*> Concurrently (runToolServer toolServerApplication liveReloadClients)
+                    <*> Concurrently (consumeGhciOutput statusServerStandardOutput statusServerErrorOutput statusServerClients)
+                    <*> Concurrently Telemetry.reportTelemetry
+                    <*> Concurrently (runFileWatcherWithDebounce (fileWatcherParams liveReloadClients databaseNeedsMigration reloadGhciVar startStatusServer))
+                    <*> Concurrently (runAppGhci ghciIsLoadingVar startStatusServer stopStatusServer statusServerStandardOutput statusServerErrorOutput statusServerClients reloadGhciVar)
 
-fileWatcherParams liveReloadClients databaseNeedsMigration databaseIsReady reloadGhciVar startStatusServer =
+fileWatcherParams liveReloadClients databaseNeedsMigration reloadGhciVar startStatusServer =
     FileWatcherParams
-        { onHaskellFileChanged = putMVar reloadGhciVar ()
-        , onSchemaChanged = concurrently_ (tryCompileSchema reloadGhciVar startStatusServer) (updateDatabaseIsOutdated databaseNeedsMigration databaseIsReady)
+        { onHaskellFileChanged = do
+            -- Use tryPutMVar to avoid blocking if a reload is already pending.
+            -- This handles the case where multiple file changes happen in quick succession.
+            void $ tryPutMVar reloadGhciVar ()
+        , onSchemaChanged = do
+            concurrently_ (tryCompileSchema reloadGhciVar startStatusServer) (updateDatabaseIsOutdated databaseNeedsMigration)
         , onAssetChanged = notifyAssetChange liveReloadClients
         }
 
-isUsingDevenv :: IO Bool
-isUsingDevenv = EnvVar.envOrDefault "IHP_DEVENV" False
-
 ghciArguments :: [String]
 ghciArguments =
     [ "-threaded"
@@ -128,35 +137,20 @@
     , "-package-env -" -- Do not load `~/.ghc/arch-os-version/environments/name file`, global packages interfere with our packages
     , "-ignore-dot-ghci" -- Ignore the global ~/.ghc/ghci.conf That file sometimes causes trouble (specifically `:set +c +s`)
     , "-ghci-script", ".ghci" -- Because the previous line ignored default ghci config file locations, we have to manual load our .ghci
-    , "+RTS", "-A128m", "-n2m", "-H2m", "--nonmoving-gc", "-N"
+    , "+RTS", "-A64m", "-n4m", "-H256m", "--nonmoving-gc", "-Iw60", "-N4"
     ]
 
 withGHCI :: (?context :: Context) => (Handle -> Handle -> Handle -> Process.ProcessHandle -> IO a) -> IO a
 withGHCI callback = do
-    let params = (procDirenvAware "ghci" ghciArguments)
+    baseParams <- procDirenvAware "ghci" ghciArguments
+    let params = baseParams
             { Process.std_in = Process.CreatePipe
             , Process.std_out = Process.CreatePipe
             , Process.std_err = Process.CreatePipe
-            , Process.create_group = True
             }
 
     Process.withCreateProcess params \(Just input) (Just output) (Just error) processHandle -> callback input output error processHandle
 
--- | Exit with an error if running as the root user
---
--- When the dev server starts the postgres server, it will fail if run as root:
---
--- > initdb: cannot be run as root
---
--- This is a bit hard to debug, therefore we proactively fail early when run as root
---
-ensureUserIsNotRoot :: IO ()
-ensureUserIsNotRoot = do
-    username <- EnvVar.envOrDefault "USERNAME" ("" :: ByteString)
-    when (username == "root") do
-        ByteString.hPutStrLn stderr "Cannot be run as root: The IHP dev server cannot be run with the root user because we cannot start the postgres server with a root user.\n\nPlease run this with a normal user.\nIf you need help, join the IHP Slack: https://ihp.digitallyinduced.com/Slack"
-        exitFailure
-
 initGHCICommands = 
     [ -- The app is loaded by loading .ghci, which then loads applicationGhciConfig, which triggers a ':l Main.hs'
      ":set prompt \"\"" -- Disable the prompt as this caused output such as '[38;5;208mIHP>[m Ser[v3e8r; 5s;t2a0r8tmedI' instead of 'Server started'
@@ -165,7 +159,6 @@
 
 runAppGhci :: (?context :: Context) => IORef Bool -> MVar () -> MVar (MVar ()) -> IORef [ByteString] -> IORef [ByteString] -> Clients -> MVar () -> IO ()
 runAppGhci ghciIsLoadingVar startStatusServer stopStatusServer statusServerStandardOutput statusServerErrorOutput statusServerClients reloadGhciVar = do
-    let isDebugMode = ?context.isDebugMode
     -- The app is using the `PORT` env variable for its web server
     let appPort :: Int = fromIntegral ?context.portConfig.appPort
     Env.setEnv "PORT" (show appPort)
@@ -176,50 +169,82 @@
             callback
 
     let processResult inputHandle outputHandle errorHandle processHandle result = do
-            hasSchemaCompilerError <- isJust <$> readIORef ?context.lastSchemaCompilerError
-            -- This branch blocks until .hs file change happens
+            -- Handle the result of GHCi compilation, then wait for the next file change
             case result of
-                Left failed -> takeMVar reloadGhciVar
-                Right _ | hasSchemaCompilerError -> takeMVar reloadGhciVar
+                Left failed -> do
+                    writeIORef ghciIsLoadingVar False
+                    -- Clear any stale reload signals to prevent rapid retry loops.
+                    -- This can happen when schema compilation fails and generates
+                    -- invalid files, triggering multiple file watcher events.
+                    void $ tryTakeMVar reloadGhciVar
+                    -- Wait for the next file change before retrying
+                    takeMVar reloadGhciVar
                 Right loaded -> do
-                    withoutStatusServer do
-                        withRunningApp ?context.portConfig.appPort inputHandle outputHandle errorHandle processHandle receiveAppOutput do
+                    -- Check for schema error fresh here to avoid race condition with tryCompileSchema.
+                    -- The schema compiler runs concurrently and may have set an error after GHCi loaded.
+                    hasSchemaCompilerError <- isJust <$> readIORef ?context.lastSchemaCompilerError
+                    writeIORef ghciIsLoadingVar False
+                    if hasSchemaCompilerError
+                        then do
+                            -- Don't start the app if there's a schema error - wait for fix
                             takeMVar reloadGhciVar
-
-
-                    pure ()
-            
+                        else do
+                            -- Clear any stale reload signal (e.g. from tryCompileSchema triggering
+                            -- a reload after recovering from a previous schema error)
+                            void $ tryTakeMVar reloadGhciVar
+                            -- Catch any exceptions from withRunningApp (e.g., startup timeout)
+                            -- so we can return to the status server gracefully
+                            result <- Exception.tryAny $ withoutStatusServer do
+                                withRunningApp ?context.portConfig.appPort inputHandle outputHandle errorHandle processHandle receiveAppOutput \appCrashed -> do
+                                    -- App is running, wait for next file change or app crash
+                                    race_ (takeMVar reloadGhciVar) (takeMVar appCrashed)
+                            case result of
+                                Left ex -> do
+                                    -- App startup failed, wait for a reload signal before trying again
+                                    takeMVar reloadGhciVar
+                                Right () -> pure ()
 
             writeIORef ghciIsLoadingVar True
-            
+
             -- Clear logs in web ui
             clearStatusServer statusServerStandardOutput statusServerErrorOutput statusServerClients
 
             result <- refresh inputHandle outputHandle errorHandle receiveAppOutput
-            
+
             -- reload app
             notifyHaskellChange ?context.liveReloadClients
 
             processResult inputHandle outputHandle errorHandle processHandle result
 
-    
     withGHCI \inputHandle outputHandle errorHandle processHandle -> do
         writeIORef ghciIsLoadingVar True
         withLoadedApp inputHandle outputHandle errorHandle receiveAppOutput \result -> do
             processResult inputHandle outputHandle errorHandle processHandle result
 
+-- | Read lines from a handle, accumulating output and classifying each line.
+--
+-- Races against @stopVar@ being filled — when the MVar is readable, reading stops.
+readHandleLines
+    :: MVar a                   -- ^ Race against this (stop when filled)
+    -> MVar ByteString.Builder  -- ^ Output accumulator
+    -> Handle                   -- ^ Handle to read from
+    -> (ByteString -> IO ())    -- ^ Log callback
+    -> (ByteString -> IO ())    -- ^ Line classifier/action
+    -> IO ()
+readHandleLines stopVar outputVar handle logLine onMatch = race_ (readMVar stopVar) $ forever do
+    line <- ByteString.hGetLine handle
+    modifyMVar_ outputVar (\builder -> pure (builder <> "\n" <> ByteString.byteString line))
+    logLine line
+    onMatch line
+
 withLoadedApp :: (?context :: Context) => Handle -> Handle -> Handle -> (OutputLine -> IO ()) -> ((Either LByteString LByteString) -> IO a) -> IO a
 withLoadedApp inputHandle outputHandle errorHandle logLine callback = do
     outputVar :: MVar ByteString.Builder <- newMVar ""
     resultVar :: MVar Bool <- newEmptyMVar
-    let readHandle handle logLine = race_ (readMVar resultVar) $ forever do
-            line <- ByteString.hGetLine handle
-            modifyMVar_ outputVar (\builder -> pure (builder <> "\n" <> ByteString.byteString line))
-            logLine line
-            case line of
-                line | "Failed," `isInfixOf` line -> putMVar resultVar False
-                line | "modules loaded." `isInfixOf` line -> putMVar resultVar True
-                _ -> pure ()
+    let onMatch line = case line of
+            line | "Failed," `isInfixOf` line -> putMVar resultVar False
+            line | "modules loaded." `isInfixOf` line -> putMVar resultVar True
+            _ -> pure ()
 
     let main = do
             sendGhciCommands inputHandle initGHCICommands
@@ -236,69 +261,70 @@
 
     (result, _, _) <- runConcurrently $ (,,)
         <$> Concurrently main
-        <*> Concurrently (readHandle outputHandle (\line -> logLine (StandardOutput line)))
-        <*> Concurrently (readHandle errorHandle (\line -> logLine (ErrorOutput line)))
+        <*> Concurrently (readHandleLines resultVar outputVar outputHandle (\line -> logLine (StandardOutput line)) onMatch)
+        <*> Concurrently (readHandleLines resultVar outputVar errorHandle (\line -> logLine (ErrorOutput line)) onMatch)
 
     pure result
 
-withRunningApp :: (?context :: Context) => Socket.PortNumber -> Handle -> Handle -> Handle -> Process.ProcessHandle -> (OutputLine -> IO ()) -> (IO a) -> IO a
+withRunningApp :: (?context :: Context) => Socket.PortNumber -> Handle -> Handle -> Handle -> Process.ProcessHandle -> (OutputLine -> IO ()) -> (MVar () -> IO a) -> IO a
 withRunningApp appPort inputHandle outputHandle errorHandle processHandle logLine callback = do
     outputVar :: MVar ByteString.Builder <- newMVar ""
     serverStarted :: MVar () <- newEmptyMVar
     serverStopped :: MVar () <- newEmptyMVar
-    let readHandle handle logLine = race_
-                (readMVar serverStopped)
-                (forever do
-                    line <- ByteString.hGetLine handle
-                    modifyMVar_ outputVar (\builder -> pure (builder <> "\n" <> ByteString.byteString line))
-                    logLine line
-                    case line of
-                        line | "Server started" `isInfixOf` line -> putMVar serverStarted ()
-                        _ -> pure ()
-                )
+    appCrashed :: MVar () <- newEmptyMVar
+    let onMatch line = case line of
+            line | "Server started" `isInfixOf` line -> putMVar serverStarted ()
+            line | "[[IHP_APP_CRASHED]]" `isInfixOf` line -> void $ tryPutMVar appCrashed ()
+            _ -> pure ()
 
     let startApp = do
+            -- Pass the socket file descriptor to the app so it can accept connections
+            -- on the same socket without rebinding the port.
+            -- Note: GHCi is a child process with its own FD table, so the app closing
+            -- its copy of the FD won't affect RunDevServer's copy.
+            socketFd <- Socket.unsafeFdSocket ?context.appSocket
+            sendGhciCommand inputHandle $ "System.Environment.setEnv \"IHP_SOCKET_FD\" \"" <> cs (show socketFd) <> "\""
             sendGhciCommand inputHandle "stopVar :: ClassyPrelude.MVar () <- ClassyPrelude.newEmptyMVar"
-            sendGhciCommand inputHandle "app <- ClassyPrelude.async (ClassyPrelude.race_ (ClassyPrelude.takeMVar stopVar) (main `ClassyPrelude.catch` \\(e :: SomeException) -> IHP.Prelude.putStrLn (tshow e)))"
+            sendGhciCommand inputHandle "app <- ClassyPrelude.async (ClassyPrelude.race_ (ClassyPrelude.takeMVar stopVar) (main `ClassyPrelude.catch` \\(e :: SomeException) -> IHP.Prelude.putStrLn (tshow e) >> IHP.Prelude.putStrLn \"[[IHP_APP_CRASHED]]\"))"
     let stopApp = do
             sendGhciCommand inputHandle "ClassyPrelude.putMVar stopVar ()"
             sendGhciCommand inputHandle "ClassyPrelude.cancel app"
-            waitForPortAvailable appPort
+            -- Give GHCi a moment to process the stop commands before signaling completion
+            -- This prevents the status server from starting while the app is still running
+            threadDelay 100000 -- 100ms
+            -- No need to wait for port availability - we use a persistent socket
             putMVar serverStopped ()
 
+    let waitForServerStart = do
+            -- Wait up to 60 seconds for "Server started" message
+            -- If the app crashes during startup, "Server started" will never be printed
+            maybeStarted <- timeout (60 * 1000000) (takeMVar serverStarted)
+            case maybeStarted of
+                Just () -> callback appCrashed
+                Nothing -> do
+                    logLine (ErrorOutput "App startup timed out after 60 seconds. Check for runtime errors above.")
+                    -- Throw exception to trigger bracket cleanup and return to status server
+                    Exception.throwString "App startup timeout"
+
     (result, _, _) <- runConcurrently $ (,,)
-        <$> Concurrently (Exception.bracket_ startApp stopApp (do takeMVar serverStarted; callback))
-        <*> Concurrently (readHandle outputHandle (\line -> logLine (StandardOutput line)))
-        <*> Concurrently (readHandle errorHandle (\line -> logLine (ErrorOutput line)))
+        <$> Concurrently (Exception.bracket_ startApp stopApp waitForServerStart)
+        <*> Concurrently (readHandleLines serverStopped outputVar outputHandle (\line -> logLine (StandardOutput line)) onMatch)
+        <*> Concurrently (readHandleLines serverStopped outputVar errorHandle (\line -> logLine (ErrorOutput line)) onMatch)
 
     pure result
 
-waitForPortAvailable :: Socket.PortNumber -> IO ()
-waitForPortAvailable port = do
-    isAvailable <- isPortAvailable port
-    unless isAvailable do
-        putStrLn "waitForPortAvailable: wait"
-        threadDelay 100000
-        waitForPortAvailable port
-
 refresh :: (?context :: Context) => Handle -> Handle -> Handle -> (OutputLine -> IO ()) -> IO (Either LByteString LByteString)
 refresh inputHandle outputHandle errorHandle logOutput = do
     outputVar :: MVar ByteString.Builder <- newMVar ""
     resultVar :: MVar Bool <- newEmptyMVar
-    let readHandle handle logLine = race_
-                (readMVar resultVar)
-                (forever do
-                    line <- ByteString.hGetLine handle
-                    modifyMVar_ outputVar (\builder -> pure (builder <> "\n" <> ByteString.byteString line))
-                    logLine line
-                    case line of
-                        line | "Failed," `isInfixOf` line -> putMVar resultVar False
-                        line | "modules loaded." `isInfixOf` line -> putMVar resultVar True
-                        line | "cannot find object file for module" `isInfixOf` line -> do
-                            -- https://gitlab.haskell.org/ghc/ghc/-/issues/11596
-                            sendGhciCommand inputHandle ":l"
-                        _ -> pure ()
-                )
+    let onMatch line = case line of
+            line | "Failed," `isInfixOf` line -> putMVar resultVar False
+            -- Match both "modules loaded." (initial) and "modules reloaded." (after :r)
+            line | "modules loaded." `isInfixOf` line || "modules reloaded." `isInfixOf` line -> putMVar resultVar True
+            line | "cannot find object file for module" `isInfixOf` line -> do
+                -- https://gitlab.haskell.org/ghc/ghc/-/issues/11596
+                sendGhciCommand inputHandle ":l"
+            _ -> pure ()
 
     let main = do
             sendGhciCommand inputHandle ":r"
@@ -312,8 +338,8 @@
 
     (result, _, _) <- runConcurrently $ (,,)
         <$> Concurrently main
-        <*> Concurrently (readHandle outputHandle (\line -> logOutput (StandardOutput line)))
-        <*> Concurrently (readHandle errorHandle (\line -> logOutput (ErrorOutput line)))
+        <*> Concurrently (readHandleLines resultVar outputVar outputHandle (\line -> logOutput (StandardOutput line)) onMatch)
+        <*> Concurrently (readHandleLines resultVar outputVar errorHandle (\line -> logOutput (ErrorOutput line)) onMatch)
 
     pure result
 
@@ -330,10 +356,10 @@
     diff <- MigrationGenerator.diffAppDatabase True databaseUrl
     pure (not (isEmpty diff))
 
-updateDatabaseIsOutdated :: (?context :: Context) => IORef Bool -> MVar () -> IO ()
-updateDatabaseIsOutdated databaseNeedsMigrationRef databaseIsReady = do
+updateDatabaseIsOutdated :: (?context :: Context) => IORef Bool -> IO ()
+updateDatabaseIsOutdated databaseNeedsMigrationRef = do
     result <- Exception.tryAny do
-            readMVar databaseIsReady
+            waitPostgres
             databaseNeedsMigration <- checkDatabaseIsOutdated
             writeIORef databaseNeedsMigrationRef databaseNeedsMigration
 
@@ -344,7 +370,7 @@
 tryCompileSchema :: (?context :: Context) => MVar () -> MVar () -> IO ()
 tryCompileSchema reloadGhciVar startStatusServer = do
     result <- Exception.tryAny SchemaCompiler.compile
-    
+
     case result of
         Left exception -> do
             Log.error (tshow exception)
@@ -358,4 +384,6 @@
             previouslyHadSchemaError <- isJust <$> readIORef ?context.lastSchemaCompilerError
             writeIORef ?context.lastSchemaCompilerError Nothing
 
-            when previouslyHadSchemaError (putMVar reloadGhciVar ())
+            -- Use tryPutMVar to avoid double trigger if file watcher already triggered reload
+            -- This triggers a reload only if recovering from a previous schema error
+            when previouslyHadSchemaError $ void $ tryPutMVar reloadGhciVar ()
diff --git a/ihp-ide.cabal b/ihp-ide.cabal
--- a/ihp-ide.cabal
+++ b/ihp-ide.cabal
@@ -1,9 +1,10 @@
 cabal-version:       2.2
 name:                ihp-ide
-version:             1.4.0
+version:             1.5.0
 synopsis:            Dev tools for IHP
 description:         The Integrated Haskell Platform is a full stack framework focused on rapid application development while striving for robust code quality.
 license:             MIT
+license-file:        LICENSE
 author:              digitally induced GmbH
 maintainer:          hello@digitallyinduced.com
 homepage:            https://ihp.digitallyinduced.com/
@@ -13,6 +14,7 @@
 stability:           Stable
 tested-with:         GHC == 9.8.4
 build-type:          Simple
+extra-source-files:  changelog.md
 data-dir: data
 data-files:
       lib/IHP/Makefile.dist
@@ -38,7 +40,8 @@
             , classy-prelude
             , mono-traversable
             , transformers
-            , directory
+            , directory >= 1.3.8.0
+            , filepath >= 1.5
             , string-conversions
             , warp
             , wai
@@ -47,15 +50,14 @@
             , wai-extra
             , inflections
             , text
-            , postgresql-simple
             , wai-app-static
             , wai-util
             , bytestring
             , network-uri
             , uri-encode
             , aeson
-            , wai-session
-            , wai-session-clientsession
+            , wai-session-maybe
+            , wai-session-clientsession-deferred
             , clientsession
             , vault
             , data-default
@@ -76,11 +78,11 @@
             , websockets
             , wai-websockets
             , wreq
-            , parser-combinators
             , neat-interpolation
             , unagi-chan
             , with-utf8
             , ihp-hsx
+            , ihp-postgres-parser
             -- Used for 'Control.Debounce' in 'IHP.IDE.FileWatcher'
             , auto-update
             , base16-bytestring
@@ -88,6 +90,11 @@
             , http-types
             , safe-exceptions
             , countable-inflections
+            , ihp-migrate
+            , hasql
+            , hasql-pool
+            , hasql-dynamic-statements
+            , hasql-implicits
     default-extensions:
         OverloadedStrings
         , NoImplicitPrelude
@@ -111,15 +118,15 @@
     if flag(FastBuild)
         ghc-options:
             -threaded
-            +RTS -A256m -n4m -H512m -qg -N -RTS
 
             -Wunused-imports
             -Wunused-foralls
-            -Wmissing-fields
+            -Werror=missing-fields
             -Winaccessible-code
             -Wmissed-specialisations
             -Wall-missed-specialisations
             -Wno-ambiguous-fields
+            -Werror=incomplete-patterns
     else
         ghc-options:
             -fstatic-argument-transformation
@@ -129,19 +136,21 @@
 
             -Wunused-imports
             -Wunused-foralls
-            -Wmissing-fields
+            -Werror=missing-fields
             -Winaccessible-code
             -Wmissed-specialisations
             -Wall-missed-specialisations
             -fspecialise-aggressively
             -Wno-ambiguous-fields
+            -Werror=incomplete-patterns
 
 library
     import: shared-properties
     hs-source-dirs: .
-    build-depends: ihp
+    build-depends: ihp, ihp-log, ihp-modal, wai-request-params, ihp-schema-compiler
     exposed-modules:
-          IHP.IDE.SchemaDesigner.Controller.Columns
+          IHP.IDE.Prelude
+        , IHP.IDE.SchemaDesigner.Controller.Columns
         , IHP.IDE.SchemaDesigner.Controller.EnumValues
         , IHP.IDE.SchemaDesigner.Controller.Enums
         , IHP.IDE.SchemaDesigner.Controller.Helper
@@ -194,6 +203,7 @@
         , IHP.IDE.CodeGen.View.NewMigration
         , IHP.IDE.Logs.View.Logs
         , IHP.IDE.Logs.Controller
+        , IHP.IDE.Logs.ServiceLog
         , IHP.IDE.CodeGen.View.NewMail
         , IHP.IDE.ToolServer
         , IHP.IDE.ToolServer.Helper.Controller
@@ -208,6 +218,7 @@
         , IHP.IDE.CodeGen.ControllerGenerator
         , IHP.IDE.CodeGen.ScriptGenerator
         , IHP.IDE.CodeGen.Types
+        , IHP.IDE.CodeGen.DefaultUuidFunction
         , IHP.IDE.CodeGen.ViewGenerator
         , IHP.IDE.CodeGen.JobGenerator
         , IHP.IDE.CodeGen.View.NewJob
@@ -218,26 +229,29 @@
         , IHP.IDE.PortConfig
         , IHP.IDE.Postgres
         , IHP.IDE.StatusServer
-        , IHP.SchemaCompiler
         , IHP.IDE.SchemaDesigner.Compiler
-        , IHP.IDE.SchemaDesigner.Parser
         , IHP.IDE.SchemaDesigner.Types
         , IHP.Telemetry
         , IHP.Version
-        , IHP.Test.Database
     autogen-modules: Paths_ihp_ide
     other-modules: Paths_ihp_ide
 
 executable RunDevServer
     import: shared-properties
-    build-depends: ihp, ihp-ide
+    build-depends: ihp, ihp-log, ihp-ide, ihp-schema-compiler
     hs-source-dirs: exe
     main-is: IHP/IDE/DevServer.hs
+    ghc-options: -rtsopts=all
     if flag(FastBuild)
         ghc-options:
             -threaded
-            +RTS -A512m -n4m -H512m -G3 -qg -N -RTS
+            -with-rtsopts=-A512m
+            -with-rtsopts=-N
+            -with-rtsopts=-n4m
+            -with-rtsopts=--nonmoving-gc
+            -with-rtsopts=-Iw60
             -Wno-ambiguous-fields
+            -Werror=incomplete-patterns
     else
         ghc-options:
             -fconstraint-solver-iterations=100
@@ -256,8 +270,9 @@
             -with-rtsopts=-N
             -with-rtsopts=-n4m
             -with-rtsopts=--nonmoving-gc
-            +RTS -A256m -n4m -H512m -G3 -qg -N -RTS
+            -with-rtsopts=-Iw60
             -Wno-ambiguous-fields
+            -Werror=incomplete-patterns
     autogen-modules: Paths_ihp_ide
     other-modules: Paths_ihp_ide
 
@@ -267,12 +282,6 @@
     hs-source-dirs: exe
     main-is: IHP/CLI/NewApplication.hs
 
-executable build-generated-code
-    import: shared-properties
-    build-depends: ihp, ihp-ide
-    hs-source-dirs: exe
-    main-is: IHP/CLI/BuildGeneratedCode.hs
-
 executable new-controller
     import: shared-properties
     build-depends: ihp, ihp-ide
@@ -314,3 +323,24 @@
 source-repository head
     type:     git
     location: https://github.com/digitallyinduced/ihp
+
+test-suite tests
+    import: shared-properties
+    type: exitcode-stdio-1.0
+    main-is: Test/Main.hs
+    hs-source-dirs: .
+    build-depends: ihp, ihp-log, ihp-modal, ihp-schema-compiler, hspec, wai-request-params, wai-asset-path
+    other-modules:
+        Test.IDE.SchemaDesigner.CompilerSpec
+        Test.IDE.SchemaDesigner.ParserSpec
+        Test.IDE.SchemaDesigner.Controller.EnumValuesSpec
+        Test.IDE.SchemaDesigner.Controller.HelperSpec
+        Test.IDE.SchemaDesigner.Controller.ValidationSpec
+        Test.IDE.CodeGeneration.ControllerGenerator
+        Test.IDE.CodeGeneration.ViewGenerator
+        Test.IDE.CodeGeneration.MailGenerator
+        Test.IDE.CodeGeneration.JobGenerator
+        Test.IDE.SchemaDesigner.SchemaOperationsSpec
+        Test.IDE.CodeGeneration.MigrationGenerator
+        Test.SchemaCompilerSpec
+        Test.ServerSpec
