{"id":23461136,"url":"https://github.com/byteally/dbrecord-opaleye","last_synced_at":"2025-04-14T05:52:06.549Z","repository":{"id":102291844,"uuid":"65550647","full_name":"byteally/dbrecord-opaleye","owner":"byteally","description":null,"archived":false,"fork":false,"pushed_at":"2016-10-28T16:00:08.000Z","size":35,"stargazers_count":28,"open_issues_count":3,"forks_count":3,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-03-27T19:49:46.219Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/byteally.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-08-12T12:11:21.000Z","updated_at":"2021-02-25T16:15:54.000Z","dependencies_parsed_at":null,"dependency_job_id":"54d59331-39ba-4aa3-96a1-e0b0f192b6ec","html_url":"https://github.com/byteally/dbrecord-opaleye","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byteally%2Fdbrecord-opaleye","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byteally%2Fdbrecord-opaleye/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byteally%2Fdbrecord-opaleye/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/byteally%2Fdbrecord-opaleye/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/byteally","download_url":"https://codeload.github.com/byteally/dbrecord-opaleye/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248830389,"owners_count":21168272,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-24T07:29:11.276Z","updated_at":"2025-04-14T05:52:06.540Z","avatar_url":"https://github.com/byteally.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# dbrecord-opaleye\n\n[![Build Status](https://travis-ci.org/byteally/dbrecord-opaleye.svg?branch=master)](https://travis-ci.org/byteally/dbrecord-opaleye)\n\n*dbrecord-opaleye* is a haskell library that\n* Lets you share models across different layers (DB, API, App layer) \n* Automatically takes care of changing the shape of your model depending on the operation. (Projection, aggregation, left join)\n* Provides sugar on top for Opaleye. dbrecord-opaleye enables you to fall back to plain Opaleye wherever you wish. \n\nConsider an example where you represent your user by the data type `User f`\n\n```haskell\ndata User f = User\n  { user_id :: Col f \"user_id\" Int\n  , name    :: Col f \"name\" Text\n  , age     :: Col f \"age\" Age\n  , gender  :: Col f \"gender\" Gender\n } deriving (Generic)\n```\n\n* When you create a query with dbrecord-opaleye selecting all columns you would get\n\n  `(User Op)`\n\n  Op (code for Opaleye) in the above code snippet indicates all the fields within `User` are db values.\n  For example Int is represented as PGInt4 in Opaleye. So in `User Op`, `user_id` 's type would be PGInt4  \n\n* When you run the query using `getAll` you would get \n\n  `(User Hask)`\n\n  Hask indicates that all the fields within `User` are normal haskell values and in this case `userId` 's type would be `Int`\n\n* On projection of name, age you would get \n\n  `User (Prj Op '[\"name\", \"age\"])`\n\n  All those fields that are not projected would have the type Void of the field's type. We have not projected Gender and user_id,  hence their types would be Void Gender and Void user_id respectively.\n\n\u003e Please note that *dbrecord-opaleye* works only with GHC 8 \u0026 above\n\nBenefits\n---------\nAlmost all the ORMs conveniently ignore projections or lie about the types by having it as nullable in all scenarios to accommodate projection. This gives the caller the wrong nullability information about the field. With dbrecord-opaleye you can project any subset of the table and still work with the type defined for that table. This greatly reduced verbosity since you dont have to maintain n different combinations of a type depending on the operations you perform with them and also the caller will always clear indication of the shape of the data. For example they can't access the voided out field and do any meaningful operation. Doing so would result in a type error.\n\nExample\n----------\n```haskell\n-- You can create newtype to encode all domain specific \n-- information and use it seamlessly as a DB type enhancing type safety\n\nnewtype Age = Age {getAge :: Int}\n  deriving (Generic)\n\nnewtype UserId = UserId {getUserId :: Int}\n  deriving (Generic)\n\nnewtype AddressId = AddressId {getAddressId :: Int}\n  deriving (Generic)\n\ndata Gender = Male | Female | Other\n  deriving (Generic, Show, Read)  \n\n-- Our models\ndata User f = User\n  { user_id    :: Col f \"user_id\" UserId\n  , name       :: Col f \"name\" Text\n  , age        :: Col f \"age\" Age\n  , gender     :: Col f \"gender\" Gender\n  , address_id :: Col f \"address_id\" AddressId\n } deriving (Generic)\n\ndata Address f = Address \n { address_id  :: Col f \"address_id\" AddressId\n , street_1    :: Col f \"street_1\" Text\n , street_2    :: Col f \"street_2\" (Maybe Text)\n , city        :: Col f \"city\" Text\n , postal_code :: Col f \"postal_code\" Text\n\n} deriving (Generic)\n\n```\n\nThe schema information of the database can be defined using the following instances\nwhich are part of [dbrecord](https://github.com/byteally/dbrecord). `dbrecord-opaleye` is built on top of [dbrecord](https://github.com/byteally/dbrecord)\n\n```haskell\n-- type for our DB\ndata TestDB\n\ninstance Database TestDB where\n  type Tables TestDB = '[User Hask, Address Hask]\n  type Types TestDB  = '[Gender]\n\ninstance Table TestDB (User Hask) where\n  type HasDefault (User Hask) = '[\"user_id\"]\n\ninstance Table TestDB (Address Hask)\n\n```\n\nMore query examples\n------------------\n```haskell\n\n-- A simple straight forward query on the user table \n-- would give us (User Op)\nuserQ :: Query (User Op)\nuserQ = proc () -\u003e do\n  u \u003c- query (Tab @TestDB @User) -\u003c ()\n  restrict -\u003c name u .== constant (T.pack \"brian\")\n  restrict -\u003c age u .== constant (Age 21)\n  returnA -\u003c u\n\n-- Let's select project just the name and age\n-- userPrjQ :: QueryArr () (User (Prj Op '[\"age\", \"name\"]))\nuserPrjQ = proc () -\u003e do\n  u \u003c- query (Tab @TestDB @User) -\u003c ()\n  returnA -\u003c project @'[\"age\", \"name\"] u\n\n-- Project just name from the above query userPrjQ\n-- QueryArr () (User (Prj (Prj Op '[\"age\", \"name\"]) '[\"name\"]))\nuserPrjQ1 = proc () -\u003e do\n  u \u003c- userPrjQ -\u003c ()\n  returnA -\u003c project @'[\"name\"] u\n\n```\n\nMoving to the Hask layer from DB layer using getAll\n---------------------------------------------------\n\n```haskell\n\n-- getAll on UserQ would transform (User Op) to User Hask\nuserPG :: PG [User Hask]\nuserPG = getAll userQ\n\nuserPrj :: PG _ -- [User (Prj Hask '[\"age\", \"name\"])]\nuserPrj = getAll userPrjQ\n\nuserTrans :: ReaderT (Config a) IO [User Hask]\nuserTrans = runTransaction userPG\n\nuserPGIO :: ReaderT (Config a) PG [User Hask]\nuserPGIO = getAll userQ\n```\n\nLeft join\n---------\n```haskell\n\naddressQ :: Query (Address Op)\naddressQ = proc () -\u003e do\n  add \u003c- query (Tab @TestDB @Address) -\u003c ()\n  returnA -\u003c add\n\n\n-- userLJoinQ :: Query (LJTabs User Address (LJ Op Op))\nuserLJoinQ = leftJoin userQ addressQ ( (\\(u, a) -\u003e user'AddressId u .==  addr'AddressId a))\n\n-- NOTE : The below where condition would give compilation error as we didnt project address ID\n-- userLJoinQ' = leftJoin userPrjQ addressQ  ((\\(u, a) -\u003e user'AddressId u .==  addr'AddressId a))\n\n-- userLJoinQ' :: Query (LJTabs User Address (LJ (Prj Op '[\"age\", \"name\"]) Op))\nuserLJoinQ' = leftJoin userPrjQ addressQ (const $ (constant True :: ReadSpec Bool))\n\n\n-- userLJoinQ'' :: Query (LJTabs User User (LJ Op (Prj Op '[\"age\", \"name\"])))\nuserLJoinQ'' = leftJoin userQ userPrjQ (const $ (constant True :: ReadSpec Bool))\n\n\n-- userLJPrjQ :: QueryArr () (User (Prj Op '[\"age\", \"name\"]))\nuserLJPrjQ = proc () -\u003e do\n  ljres \u003c- userLJoinQ' -\u003c ()\n  returnA -\u003c leftTab ljres\n\n-- userLJPrjQ' :: QueryArr () (User (Prj Op '[\"age\", \"name\"]))\nuserLJPrjQ' = proc () -\u003e do\n  u \u003c- userPrjQ -\u003c ()\n  ljres \u003c- userLJoinQ' -\u003c ()\n  returnA -\u003c leftTab ljres  \n\n-- userLJoin :: ReaderT (Config a) PG [LJTabs User Address (LJ Hask Hask)]\nuserLJoin = getAll userLJoinQ :: ReaderT (Config a) PG _\n\n```\n\nAggregation\n-----------\n\n```haskell\n-- userAggQ1 :: Query (User (Agg Op '['Sum \"user_id\", 'GroupBy \"name\"]))\nuserAggQ1 = aggregate @'[Sum \"user_id\", GroupBy \"name\"] (Tab @TestDB @User) userQ\n\n-- userAggQ2 :: Query (User (Agg (Prj Op '[\"age\", \"name\"]) '['Sum \"age\", 'GroupBy \"name\"]))\nuserAggQ2 = aggregate @'[Sum \"age\", GroupBy \"name\"] (Tab @TestDB @User) userPrjQ\n\n-- userAggQ3 :: Query (User (Agg Op '['Sum \"user_id\", 'Sum \"age\", 'GroupBy \"name\"]))\nuserAggQ3 = Opaleye.Record.aggregateOrdered @'[Sum \"user_id\", Sum \"age\", GroupBy \"name\"] (Tab @TestDB @User) (asc name) userQ\n\n-- userAggQ4 :: Query (User (Agg (Prj (Prj Op '[\"age\", \"name\"]) '[\"name\"]) '['GroupBy \"name\"]))\nuserAggQ4 = aggregate @'[GroupBy \"name\"] (Tab @TestDB @User) userPrjQ1\n\n-- userAggQ5 :: Query (User (Agg (Prj Op '[\"age\", \"name\"]) '['GroupBy \"name\"]))\nuserAggQ5 = aggregate @'[GroupBy \"name\"] (Tab @TestDB @User) userLJPrjQ'\n\n-- userAgg :: ReaderT (Config a) PG [User (Agg Hask '['Sum \"user_id\", 'GroupBy \"name\"])]\nuserAgg = getAll userAggQ1 :: ReaderT (Config a) PG _\n\n```\n\nInsert, Update, Delete\n--------------------------\n\n```haskell\n-- Note that we can send Nothing in place of userId as we declared that it has a default value earlier\n-- uUserRow :: HaskW TestDB User\n-- HaskW stands for Haskell writable\nuUserRow = User Nothing (T.pack \"brian\") (Age 21) Male (AddressId 1) :: HaskW TestDB User\n\n-- userInsert :: ReaderT (Config a) PG ()\nuserInsert = insert (Tab @TestDB @User) uUserRow :: ReaderT (Config a) PG ()\n\n-- userInsert' :: ReaderT (Config a) PG [User Hask]\nuserInsert' = insertRet (Tab @TestDB @User) uUserRow id :: ReaderT (Config a) PG [User Hask]\n\n-- userInsert'' :: ReaderT (Config a) PG [User (Prj Hask '[\"name\"])]\nuserInsert'' = insertRet (Tab @TestDB @User) uUserRow (project @'[\"name\"]) :: ReaderT (Config a) PG [User _]\n\n-- userDelete :: ReaderT (Config a) PG Int64\nuserDelete = delete (Tab @TestDB @User) (\\_ -\u003e constant False) :: ReaderT (Config a) PG Int64\n\n-- userUpdate :: ReaderT (Config a) PG Int64\nuserUpdate = update (Tab @TestDB @User) (\\_ -\u003e constant uUserRow) (\\_ -\u003e constant False) :: ReaderT (Config a) PG Int64\n\n-- Update and return the entire user \n-- userUpdate' :: ReaderT (Config a) PG [User Hask]\nuserUpdate' = updateRet (Tab @TestDB @User) (\\_ -\u003e constant uUserRow) (\\_ -\u003e constant False) id :: ReaderT (Config a) PG [User Hask]\n\n-- Update and project user's age\n-- userUpdate'' :: ReaderT (Config a) PG [User (Prj Hask '[\"age\"])]\nuserUpdate'' = updateRet (Tab @TestDB @User) (\\_ -\u003e constant uUserRow) (\\_ -\u003e constant False) (project @'[\"age\"]) :: ReaderT (Config a) PG [User _]\n\n```\n\nPending items\n-------------\n* Fix aggregation on multiple relation composed into another type\n* Migration feature for dbrecord\n* Check constraint expression support in dbrecord\n\n\nWould love to get your feedback and contributions are most welcome!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbyteally%2Fdbrecord-opaleye","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbyteally%2Fdbrecord-opaleye","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbyteally%2Fdbrecord-opaleye/lists"}