on master help sign in
Decode transactions with Beam
commitcab55e2fec202cb2e7ff6f45e08b04c59b799b23 authorRyan Trinkle <ryan.trinkle@gmail.com> dateTue 27 Nov 2018, 15:51 (8 years ago) parentf24e04a
8 files changed+214−21
default.nix+4 dep/beam/github.json+3−3 dep/postgresql-simple/default.nix+7 dep/postgresql-simple/github.json+7 lib/backend/src/Obelisk/Postgres/LogicalDecoding/Plugins/TestDecoding.hs+2 skeleton/backend/src/Backend.hs+161−9 skeleton/backend/src/Backend/Schema.hs+17−8 skeleton/common/src/Common/Schema.hs+13−1
default.nix+4
@@ -20,6 +20,8 @@ let
2020
2121 gargoyle-src = hackGet ./dep/gargoyle;
2222
23+ postgresql-simple-src = hackGet ./dep/postgresql-simple;
24+
2325 getReflexPlatform = getReflexPlatform' __useLegacyCompilers;
2426 getReflexPlatform' = __useLegacyCompilers: sys: import ./dep/reflex-platform {
2527 inherit iosSdkVersion __useLegacyCompilers;
@@ -73,6 +75,8 @@ let
7375 beam-postgres = dontCheck (self.callCabal2nix "beam-postgres" (beam-src + /beam-postgres) {});
7476 beam-migrate = self.callCabal2nix "beam-migrate" (beam-src + /beam-migrate) {};
7577
78+ postgresql-simple = dontCheck (self.callCabal2nix "postgresql-simple" postgresql-simple-src {});
79+
7680 websockets = self.callCabal2nix "websockets" (pkgs.fetchFromGitHub {
7781 owner = "obsidiansystems";
7882 repo = "websockets";
dep/beam/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "obsidiansystems",
33 "repo": "beam",
4− "branch": "int-and-serial-size",
5− "rev": "87caf239ea198bad03acb73e65f9a43566ee4373",
6− "sha256": "1lnzwds131cxix0dwkql980w9s2jdxfp2b9q7x8d4wh5i5jfbh6g"
4+ "branch": "obelisk",
5+ "rev": "d060ed41016aa62f4a571e69430f64ef660db894",
6+ "sha256": "0ag19lc2izay5qg3j9mv44izsccglh89191gmh9vfx2kqv20h945"
77}
dep/postgresql-simple/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/postgresql-simple/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "obsidiansystems",
3+ "repo": "postgresql-simple",
4+ "branch": "synthesizable-fields",
5+ "rev": "dc162dc21f586ba4c6e5c44f92b76aac653a85db",
6+ "sha256": "1mpx6w94z9b3vv1981v7csj2y4p97irv3p7cvv1lyz9c595kkisp"
7+}
lib/backend/src/Obelisk/Postgres/LogicalDecoding/Plugins/TestDecoding.hs+2
@@ -4,7 +4,9 @@ module Obelisk.Postgres.LogicalDecoding.Plugins.TestDecoding
44 ( line
55 , Transaction
66 , Change (..)
7+ , Literal (..)
78 , LineError (..)
9+ , TypeName (..)
810 , linesToTransactions
911 ) where
1012
skeleton/backend/src/Backend.hs+161−9
@@ -1,4 +1,7 @@
11{-# LANGUAGE EmptyCase #-}
2+{-# LANGUAGE StandaloneDeriving #-}
3+{-# LANGUAGE TypeOperators #-}
4+{-# LANGUAGE UndecidableInstances #-}
25{-# LANGUAGE MultiParamTypeClasses #-}
36{-# LANGUAGE AllowAmbiguousTypes #-}
47{-# LANGUAGE TemplateHaskell #-}
@@ -16,13 +19,16 @@
1619{-# LANGUAGE OverloadedStrings #-}
1720{-# LANGUAGE RankNTypes #-}
1821{-# LANGUAGE TypeFamilies #-}
22+
23+{-# OPTIONS_GHC -fno-warn-orphans #-} --TODO: Eliminate this
1924module Backend where
2025
26+import Control.Exception
27+import Control.Concurrent
2128import Common.Api
2229import Common.Schema
2330import Backend.Schema
2431import Common.Route
25−import Data.Map.Monoidal (MonoidalMap (..))
2632import qualified Data.Map.Monoidal as Map
2733import Data.Pool
2834import Data.Semigroup
@@ -33,8 +39,19 @@ import Obelisk.Db
3339import Obelisk.Postgres.LogicalDecoding.Plugins.TestDecoding
3440import qualified Data.Map as OldMap
3541import Data.Sequence (Seq)
42+import Data.These
43+import Data.Text.Encoding
44+import Data.HashMap.Strict (HashMap)
45+import qualified Data.HashMap.Strict as HashMap
3646
47+import Database.PostgreSQL.Simple.FromField (FromField (..), runConversion)
48+import Database.PostgreSQL.Simple.Ok
49+import Database.PostgreSQL.Simple.Internal
50+import qualified Database.PostgreSQL.Simple.TypeInfo.Static as TI
51+import qualified Database.PostgreSQL.LibPQ as PQ
3752import Database.Beam
53+import Database.Beam.Backend.Types
54+import Database.Beam.Backend.SQL.Types
3855import Database.Beam.Postgres
3956import Database.Beam.Postgres.Full (PgInsertReturning, returning, runPgInsertReturningList)
4057import Database.Beam.Postgres.Migrate
@@ -45,9 +62,10 @@ import Control.Monad.Reader
4562
4663import Data.Proxy
4764
48−import Data.Functor.Identity
49−import Data.Functor.Const
5065import Control.Lens
66+import qualified GHC.Generics as Generic
67+import GHC.Generics hiding (R, C)
68+import GHC.Types
5169
5270import Debug.Trace
5371
@@ -57,9 +75,18 @@ backend = Backend
5775 withDbUri "db" $ \dbUri -> do
5876 withConnectionPool dbUri $ \pool -> do
5977 withResource pool $ \conn -> do
60− runBeamPostgres conn $ autoMigrate migrationBackend checkedDb
78+ try (runBeamPostgres conn $ autoMigrate migrationBackend checkedDb) >>= \case
79+ Left (_ :: SomeException) -> do
80+ putStrLn "Required migration is unsafe; refusing to start"
81+ forever $ threadDelay maxBound
82+ Right _ -> pure ()
6183 let onNotify :: forall a. VS (Id Task) Task a -> Transaction -> ReadDb (V (Id Task) Task a)
6284 onNotify (VS requested) txn = do
85+ traceM "Hello"
86+ decoded <- ReadDb $ do
87+ conn <- ask
88+ lift $ decodeTransaction conn txn
89+ traceM $ show $ _db_tasks decoded
6390 tasks <- runSelectReturningList' $ select $ all_ $ _db_tasks db
6491 pure $ V $ Map.intersectionWith (,) requested $ Map.fromList $ fmap (\x -> (TaskId $ _task_id x, First $ Just x)) tasks
6592 onSubscribe :: forall a. VS (Id Task) Task a -> ReadDb (V (Id Task) Task a)
@@ -116,11 +143,136 @@ instance MonadBeamWrite WriteDb where
116143 runUpdate' = unsafeRunPgWriteDb . runUpdate
117144 runDelete' = unsafeRunPgWriteDb . runDelete
118145
119−decodeTransaction :: Transaction -> Db (Const (Seq Change))
120−decodeTransaction (_, tableChanges) = runIdentity $ zipTables (Proxy :: Proxy Postgres) f db db
146+class GPatchDatabase x where
147+ gDatabasePatchDecoder :: x
148+
149+instance GPatchDatabase (x p) => GPatchDatabase (D1 f x p) where
150+ gDatabasePatchDecoder = M1 gDatabasePatchDecoder
151+
152+instance GPatchDatabase (x p) => GPatchDatabase (C1 f x p) where
153+ gDatabasePatchDecoder = M1 gDatabasePatchDecoder
154+
155+instance (GPatchDatabase (x p), GPatchDatabase (y p)) => GPatchDatabase ((x :*: y) p) where
156+ gDatabasePatchDecoder = gDatabasePatchDecoder :*: gDatabasePatchDecoder
157+
158+instance PatchEntity be a => GPatchDatabase (S1 f (K1 Generic.R (EntityPatchDecoder be a)) p) where
159+ gDatabasePatchDecoder = M1 (K1 patchEntity)
160+
161+class GPatchFields x where
162+ gPatchFields :: x
163+
164+instance GPatchFields (x p) => GPatchFields (D1 f x p) where
165+ gPatchFields = M1 gPatchFields
166+
167+instance GPatchFields (x p) => GPatchFields (C1 f x p) where
168+ gPatchFields = M1 gPatchFields
169+
170+instance (GPatchFields (x p), GPatchFields (y p)) => GPatchFields ((x :*: y) p) where
171+ gPatchFields = gPatchFields :*: gPatchFields
172+
173+instance BackendFromField be a => GPatchFields (S1 f (K1 Generic.R (FieldDecoder be a)) p) where
174+ gPatchFields = M1 (K1 FieldDecoder)
175+
176+class PatchEntity be e where
177+ patchEntity :: EntityPatchDecoder be e
178+
179+from' :: Generic x => x -> Rep x ()
180+from' = Generic.from
181+
182+to' :: Generic x => Rep x () -> x
183+to' = Generic.to
184+
185+instance ( Generic (tbl (FieldDecoder be))
186+ , GPatchFields (Rep (tbl (FieldDecoder be)) ())
187+ , Table tbl
188+ ) => PatchEntity be (TableEntity tbl) where
189+ patchEntity = EntityPatchDecoder_Table $ to' gPatchFields
190+
191+defaultDatabasePatchDecoder
192+ :: forall be db
193+ . ( Generic (db (EntityPatchDecoder be))
194+ , GPatchDatabase (Rep (db (EntityPatchDecoder be)) ())
195+ )
196+ => db (EntityPatchDecoder be)
197+defaultDatabasePatchDecoder = to' gDatabasePatchDecoder
198+
199+class PatchableEntity be e where
200+ patchDecoder :: EntityPatchDecoder be e
201+
202+data EntityPatchDecoder be e where
203+ EntityPatchDecoder_NotDecodable :: EntityPatchDecoder be e
204+ EntityPatchDecoder_Table :: Table tbl => tbl (FieldDecoder be) -> EntityPatchDecoder be (TableEntity tbl)
205+
206+data FieldDecoder be a where
207+ FieldDecoder :: BackendFromField be a => FieldDecoder be a
208+
209+data EntityPatch e where
210+ EntityPatch_NotPatchable :: EntityPatch e
211+ EntityPatch_Table :: Seq (These (PrimaryKey tbl Maybe) (tbl Maybe)) -> EntityPatch (TableEntity tbl) --TODO: Add old keys / deletes
212+
213+deriving instance (Show (PrimaryKey tbl Maybe), Show (tbl Maybe)) => Show (EntityPatch (TableEntity tbl))
214+
215+deriving instance FromField a => FromField (SqlSerial a)
216+
217+decodeRow
218+ :: forall tbl tbl'
219+ . Beamable tbl
220+ => Connection
221+ -> tbl (TableField tbl')
222+ -> tbl (FieldDecoder Postgres)
223+ -> HashMap Identifier (TypeName, Maybe Literal)
224+ -> IO (tbl Maybe)
225+decodeRow conn settings decoders r = zipBeamFieldsM d settings decoders
226+ where d :: forall a. Columnar' (TableField tbl') a -> Columnar' (FieldDecoder Postgres) a -> IO (Columnar' Maybe a)
227+ d (Columnar' fieldSettings) (Columnar' FieldDecoder) = do
228+ let n = _fieldName fieldSettings
229+ result <- case HashMap.lookup (Identifier n) r of
230+ Just (TypeName typeName, v) -> do
231+ let f = Field
232+ { typeOid = case typeName of
233+ "text" -> TI.typoid TI.text
234+ "int8" -> TI.typoid TI.int8
235+ "bigint" -> TI.typoid TI.int8
236+ "character varying" -> TI.typoid TI.text
237+ "boolean" -> TI.typoid TI.bool
238+ _ -> error $ "Unrecognized type: " <> show typeName --TODO: Retrieve types properly
239+ , format = PQ.Text --TODO: Is this right?
240+ , tableOid = Nothing
241+ , name = Just $ encodeUtf8 n
242+ }
243+ case v of
244+ Just (Literal_Present l) -> Just <$> runConversion (fromField f $ Just l) conn
245+ Nothing -> Just <$> runConversion (fromField f Nothing) conn
246+ Just Literal_UnchangedToastDatum -> pure Nothing
247+ Nothing -> pure Nothing
248+ case result of
249+ Just (Ok a) -> pure $ Columnar' $ Just a
250+ Just (Errors e) -> fail $ show e
251+ Nothing -> pure $ Columnar' Nothing
252+
253+decodeChange
254+ :: forall tbl
255+ . Table tbl
256+ => Connection
257+ -> tbl (TableField tbl)
258+ -> tbl (FieldDecoder Postgres)
259+ -> Change
260+ -> IO (These (PrimaryKey tbl Maybe) (tbl Maybe))
261+decodeChange conn settings decoders = \case
262+ Change_Insert r -> That <$> decodeRow conn settings decoders r
263+ Change_Update k r -> These <$> decodeRow conn (primaryKey settings) (primaryKey decoders) (fmap (fmap Just) k) <*> decodeRow conn settings decoders r
264+ Change_Delete k -> This <$> decodeRow conn (primaryKey settings) (primaryKey decoders) (fmap (fmap Just) k)
265+
266+decodeTransaction :: Connection -> Transaction -> IO (Db EntityPatch)
267+decodeTransaction conn (_, tableChanges) = zipTables (Proxy :: Proxy Postgres) f db (defaultDatabasePatchDecoder @Postgres @Db)
121268 where
122− f :: IsDatabaseEntity Postgres entityType => DatabaseEntity Postgres Db entityType -> DatabaseEntity Postgres Db entityType -> Identity (Const (Seq Change) entityType)
123− f (DatabaseEntity descriptor) _ = pure $ Const $
269+ f :: forall entityType. IsDatabaseEntity Postgres entityType => DatabaseEntity Postgres Db entityType -> EntityPatchDecoder Postgres entityType -> IO (EntityPatch entityType)
270+ f (DatabaseEntity descriptor) decoder =
124271 let n = view dbEntityName descriptor
125272 s = Just "public" -- view dbEntitySchema descriptor --TODO: Beam has 'Nothing' while the replication connection reports 'Just "public"'
126− in trace (show (s, n)) $ OldMap.findWithDefault mempty (QualifiedIdentifier s n) tableChanges
273+ vals = OldMap.findWithDefault mempty (QualifiedIdentifier s n) tableChanges
274+ x :: IO (EntityPatch entityType)
275+ x = case decoder of
276+ EntityPatchDecoder_NotDecodable -> pure EntityPatch_NotPatchable
277+ EntityPatchDecoder_Table decoders -> EntityPatch_Table <$> traverse (decodeChange conn (dbTableSettings descriptor) decoders) vals
278+ in x
skeleton/backend/src/Backend/Schema.hs+17−8
@@ -1,4 +1,5 @@
11{-# LANGUAGE StandaloneDeriving #-}
2+{-# LANGUAGE TypeApplications #-}
23{-# LANGUAGE UndecidableInstances #-}
34{-# LANGUAGE ScopedTypeVariables #-}
45{-# LANGUAGE MultiParamTypeClasses #-}
@@ -6,26 +7,34 @@
67{-# LANGUAGE FlexibleInstances #-}
78{-# LANGUAGE DeriveGeneric #-}
89{-# LANGUAGE TypeSynonymInstances #-}
10+
11+{-# OPTIONS_GHC -fno-warn-orphans #-} --TODO: Eliminate this
912module Backend.Schema where
1013
1114import Common.Schema
1215
16+import Control.Lens
17+import Data.List.NonEmpty (NonEmpty (..))
18+import Data.Proxy
19+import qualified Data.Text as T
1320import Database.Beam
21+import Database.Beam.Schema.Tables
1422import Database.Beam.Migrate.Simple
1523import Database.Beam.Migrate
1624import Database.Beam.Postgres
1725
18−data Db f = Db
19− { _db_tasks :: f (TableEntity TaskT)
20− }
21− deriving (Generic)
22−
23−deriving instance Show (f (TableEntity TaskT)) => Show (Db f)
24−
2526instance Database Postgres Db
2627
28+replaceAllFieldNames :: forall be db. Database be db => CheckedDatabaseSettings be db -> CheckedDatabaseSettings be db
29+replaceAllFieldNames d = runIdentity $ zipTables (Proxy @be) (\_ -> pure . f) d d
30+ where f :: IsDatabaseEntity be e => CheckedDatabaseEntity be db e -> CheckedDatabaseEntity be db e
31+ f (CheckedDatabaseEntity descriptor preds) =
32+ let renameEntity = unChecked . dbEntityName %~ T.drop 1 . T.dropWhile (/= '_')
33+ renameFields = unChecked %~ withFieldRenamer (renamingFields $ \(x :| _) -> T.drop 1 $ T.dropWhile (/= '_') $ T.drop 1 x)
34+ in CheckedDatabaseEntity (renameFields $ renameEntity descriptor) preds
35+
2736checkedDb :: CheckedDatabaseSettings Postgres Db
28−checkedDb = defaultMigratableDbSettings
37+checkedDb = replaceAllFieldNames defaultMigratableDbSettings
2938
3039db :: DatabaseSettings Postgres Db
3140db = unCheckDatabase checkedDb
skeleton/common/src/Common/Schema.hs+13−1
@@ -7,6 +7,8 @@
77{-# LANGUAGE FlexibleInstances #-}
88{-# LANGUAGE DeriveGeneric #-}
99{-# LANGUAGE TypeSynonymInstances #-}
10+
11+{-# OPTIONS_GHC -fno-warn-orphans #-} --TODO: Eliminate this
1012module Common.Schema where
1113
1214import Data.Aeson
@@ -23,7 +25,8 @@ data TaskT f = Task
2325 deriving (Generic)
2426type Task = TaskT Identity
2527
26−deriving instance Show Task
28+deriving instance (Show (Columnar f (SqlSerial Int64)), Show (Columnar f Text), Show (Columnar f Bool)) => Show (TaskT f)
29+
2730deriving instance Eq Task
2831
2932instance Beamable TaskT
@@ -33,6 +36,8 @@ instance Table TaskT where
3336 deriving (Generic)
3437 primaryKey = TaskId . _task_id
3538
39+deriving instance Show (Columnar f (SqlSerial Int64)) => Show (PrimaryKey TaskT f)
40+
3641deriving instance ToJSONKey a => ToJSONKey (SqlSerial a)
3742deriving instance FromJSONKey a => FromJSONKey (SqlSerial a)
3843
@@ -46,3 +51,10 @@ instance ToJSON (TaskT Identity)
4651instance FromJSON (TaskT Identity)
4752
4853instance Beamable (PrimaryKey TaskT)
54+
55+data Db f = Db
56+ { _db_tasks :: f (TableEntity TaskT)
57+ }
58+ deriving (Generic)
59+
60+deriving instance Show (f (TableEntity TaskT)) => Show (Db f)