{"id":19427175,"url":"https://github.com/morphismtech/squeal","last_synced_at":"2025-05-14T21:08:15.825Z","repository":{"id":24514503,"uuid":"101666131","full_name":"morphismtech/squeal","owner":"morphismtech","description":"Squeal, a deep embedding of SQL in Haskell","archived":false,"fork":false,"pushed_at":"2024-12-26T15:54:07.000Z","size":3947,"stargazers_count":369,"open_issues_count":30,"forks_count":32,"subscribers_count":15,"default_branch":"dev","last_synced_at":"2025-04-03T22:05:38.971Z","etag":null,"topics":["database","haskell","postgresql","sql"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/morphismtech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2017-08-28T16:55:09.000Z","updated_at":"2025-03-16T18:16:26.000Z","dependencies_parsed_at":"2023-01-14T01:06:44.309Z","dependency_job_id":"5f5c4fec-93dd-472a-b081-8e6ff7eb23b8","html_url":"https://github.com/morphismtech/squeal","commit_stats":{"total_commits":1489,"total_committers":28,"mean_commits":53.17857142857143,"dds":0.3888515782404298,"last_synced_commit":"22a8d7a693445362289144121e45eb0aeedd1a6c"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morphismtech%2Fsqueal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morphismtech%2Fsqueal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morphismtech%2Fsqueal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morphismtech%2Fsqueal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/morphismtech","download_url":"https://codeload.github.com/morphismtech/squeal/tar.gz/refs/heads/dev","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248351998,"owners_count":21089370,"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":["database","haskell","postgresql","sql"],"created_at":"2024-11-10T14:10:42.154Z","updated_at":"2025-04-11T06:19:48.964Z","avatar_url":"https://github.com/morphismtech.png","language":"Haskell","readme":"# squeal\n\n![squeal-icon](https://raw.githubusercontent.com/morphismtech/squeal/dev/squeal.gif)\n\n[![GithubWorkflowCI](https://github.com/morphismtech/squeal/actions/workflows/ci.yml/badge.svg)](https://github.com/morphismtech/squeal/actions/workflows/ci.yml)\n\n[Github](https://github.com/morphismtech/squeal)\n\n[Hackage](https://hackage.haskell.org/package/squeal-postgresql)\n\n[Stackage](https://www.stackage.org/package/squeal-postgresql)\n\n[YouTube](https://www.youtube.com/watch?v=rWfEQfAaNc4)\n\n## introduction\n\nSqueal is a deep embedding of SQL into Haskell. By \"deep embedding\", I am abusing the\nterm somewhat. What I mean is that Squeal embeds both SQL terms and SQL types\ninto Haskell at the term and type levels respectively. This leads to a very high level\nof type-safety in Squeal.\n\nSqueal embeds not just the structured query language of SQL but also the\ndata manipulation language and the data definition language; that's `SELECT`,\n`INSERT`, `UPDATE`, `DELETE`, `WITH`, `CREATE`, `DROP`, and `ALTER` commands.\n\nSqueal expressions closely match their corresponding SQL expressions so that\nthe SQL they actually generate is completely predictable. They are also highly\ncomposable and cover a large portion of SQL.\n\n## features\n\n* generic encoding of Haskell tuples and records into query parameters\n  and generic decoding of query results into Haskell records\n  using [`generics-sop`](https://hackage.haskell.org/package/generics-sop)\n* access to SQL alias system using the `OverloadedLabels` extension\n* type-safe `NULL` and `DEFAULT`\n* type-safe SQL constraints `CHECK`, `UNIQUE`, `PRIMARY KEY` and `FOREIGN KEY`\n* type-safe aggregation\n* escape hatches for writing raw SQL\n* [`mtl`](https://hackage.haskell.org/package/mtl) compatible monad transformer\n  for executing as well as preparing queries and manipulations\n  and [Atkey](https://bentnib.org/paramnotions-jfp.pdf) indexed monad transformer\n  for executing definitions.\n* linear, pure or impure, one-way or rewindable migrations\n* connection pools\n* transactions\n* views\n* array, composite and enumerated types\n* json functions and operations\n* multischema support\n* correlated subqueries\n* window functions\n* text search\n* time functions\n* ranges\n* indexes\n* inlining\n\n## installation\n\n`stack install squeal-postgresql`\n\n## testing\n\nStart postgres on localhost port `5432` and create a database named `exampledb`.\nOn macOS, you can create the database using `createdb exampledb`.\n\n`stack test`\n\n## contributing\n\nWe welcome contributors.\nPlease make pull requests on the `dev` branch instead of `master`.\nThe `Issues` page is a good place to communicate.\n\n## usage\n\nLet's see an example!\n\nFirst, we need some language extensions because Squeal uses modern GHC\nfeatures.\n\n```Haskell\n\u003e\u003e\u003e :set -XDataKinds -XDeriveGeneric -XOverloadedLabels -XFlexibleContexts\n\u003e\u003e\u003e :set -XOverloadedStrings -XTypeApplications -XTypeOperators -XGADTs\n```\n\nWe'll need some imports.\n\n```Haskell\n\u003e\u003e\u003e import Control.Monad.IO.Class (liftIO)\n\u003e\u003e\u003e import Data.Int (Int32)\n\u003e\u003e\u003e import Data.Text (Text)\n\u003e\u003e\u003e import Squeal.PostgreSQL\n```\n\nWe'll use generics to easily convert between Haskell and PostgreSQL values.\n\n```Haskell\n\u003e\u003e\u003e import qualified Generics.SOP as SOP\n\u003e\u003e\u003e import qualified GHC.Generics as GHC\n```\n\nThe first step is to define the schema of our database. This is where\nwe use `DataKinds` and `TypeOperators`.\n\n```Haskell\n\u003e\u003e\u003e :{\ntype UsersColumns =\n  '[ \"id\"   :::   'Def :=\u003e 'NotNull 'PGint4\n   , \"name\" ::: 'NoDef :=\u003e 'NotNull 'PGtext ]\ntype UsersConstraints = '[ \"pk_users\" ::: 'PrimaryKey '[\"id\"] ]\ntype EmailsColumns =\n  '[ \"id\" ::: 'Def :=\u003e 'NotNull 'PGint4\n   , \"user_id\" ::: 'NoDef :=\u003e 'NotNull 'PGint4\n   , \"email\" ::: 'NoDef :=\u003e 'Null 'PGtext ]\ntype EmailsConstraints =\n  '[ \"pk_emails\"  ::: 'PrimaryKey '[\"id\"]\n   , \"fk_user_id\" ::: 'ForeignKey '[\"user_id\"] \"public\" \"users\" '[\"id\"] ]\ntype Schema =\n  '[ \"users\" ::: 'Table (UsersConstraints :=\u003e UsersColumns)\n   , \"emails\" ::: 'Table (EmailsConstraints :=\u003e EmailsColumns) ]\ntype DB = Public Schema\n:}\n```\n\nNotice the use of type operators.\n\n`:::` is used to pair an alias `Symbol` with a `SchemasType`, a `SchemumType`,\na `TableConstraint` or a `ColumnType`. It is intended to connote Haskell's `::`\noperator.\n\n`:=\u003e` is used to pair `TableConstraints` with a `ColumnsType`,\nyielding a `TableType`, or to pair an `Optionality` with a `NullType`,\nyielding a `ColumnType`. It is intended to connote Haskell's `=\u003e` operator\n\nNext, we'll write `Definition`s to set up and tear down the schema. In\nSqueal, a `Definition` like `createTable`, `alterTable` or `dropTable`\nhas two type parameters, corresponding to the schema\nbefore being run and the schema after. We can compose definitions using `\u003e\u003e\u003e`.\nHere and in the rest of our commands we make use of overloaded\nlabels to refer to named tables and columns in our schema.\n\n```Haskell\n\u003e\u003e\u003e :{\nlet\n  setup :: Definition (Public '[]) DB\n  setup =\n    createTable #users\n      ( serial `as` #id :*\n        (text \u0026 notNullable) `as` #name )\n      ( primaryKey #id `as` #pk_users ) \u003e\u003e\u003e\n    createTable #emails\n      ( serial `as` #id :*\n        (int \u0026 notNullable) `as` #user_id :*\n        (text \u0026 nullable) `as` #email )\n      ( primaryKey #id `as` #pk_emails :*\n        foreignKey #user_id #users #id\n          (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_user_id )\n:}\n```\n\nWe can easily see the generated SQL is unsurprising looking.\n\n```Haskell\n\u003e\u003e\u003e printSQL setup\n```\n```SQL\nCREATE TABLE \"users\" (\"id\" serial, \"name\" text NOT NULL, CONSTRAINT \"pk_users\" PRIMARY KEY (\"id\"));\nCREATE TABLE \"emails\" (\"id\" serial, \"user_id\" int NOT NULL, \"email\" text NULL, CONSTRAINT \"pk_emails\" PRIMARY KEY (\"id\"), CONSTRAINT \"fk_user_id\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE);\n```\n\nNotice that `setup` starts with an empty public schema `(Public '[])` and produces `DB`.\nIn our `createTable` commands we included `TableConstraint`s to define\nprimary and foreign keys, making them somewhat complex. Our `teardown`\n`Definition` is simpler.\n\n```Haskell\n\u003e\u003e\u003e :{\nlet\n  teardown :: Definition DB (Public '[])\n  teardown = dropTable #emails \u003e\u003e\u003e dropTable #users\n:}\n\n\u003e\u003e\u003e printSQL teardown\n```\n```SQL\nDROP TABLE \"emails\";\nDROP TABLE \"users\";\n```\n\nWe'll need a Haskell type for `User`s. We give the type `Generics.SOP.Generic` and\n`Generics.SOP.HasDatatypeInfo` instances so that we can encode and decode `User`s.\n\n```Haskell\n\u003e\u003e\u003e :set -XDerivingStrategies -XDeriveAnyClass\n\u003e\u003e\u003e :{\ndata User = User { userName :: Text, userEmail :: Maybe Text }\n  deriving stock (Show, GHC.Generic)\n  deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)\n:}\n```\n\nNext, we'll write `Statement`s to insert `User`s into our two tables.\nA `Statement` has three type parameters, the schemas it refers to,\ninput parameters and an output row. When\nwe insert into the users table, we will need a parameter for the `name`\nfield but not for the `id` field. Since it's serial, we can use a default\nvalue. However, since the emails table refers to the users table, we will\nneed to retrieve the user id that the insert generates and insert it into\nthe emails table. We can do this in a single `Statement` by using a\n`with` `manipulation`.\n\n```Haskell\n\u003e\u003e\u003e :{\nlet\n  insertUser :: Statement DB User ()\n  insertUser = manipulation $ with (u `as` #u) e\n    where\n      u = insertInto #users\n        (Values_ (Default `as` #id :* Set (param @1) `as` #name))\n        OnConflictDoRaise (Returning_ (#id :* param @2 `as` #email))\n      e = insertInto_ #emails $ Select\n        (Default `as` #id :* Set (#u ! #id) `as` #user_id :* Set (#u ! #email) `as` #email)\n        (from (common #u))\n:}\n```\n\n```Haskell\n\u003e\u003e\u003e printSQL insertUser\n```\n```SQL\nWITH \"u\" AS (INSERT INTO \"users\" (\"id\", \"name\") VALUES (DEFAULT, ($1 :: text)) RETURNING \"id\" AS \"id\", ($2 :: text) AS \"email\") INSERT INTO \"emails\" (\"user_id\", \"email\") SELECT \"u\".\"id\", \"u\".\"email\" FROM \"u\" AS \"u\"\n```\n\nNext we write a `Statement` to retrieve users from the database. We're not\ninterested in the ids here, just the usernames and email addresses. We\nneed to use an `innerJoin` to get the right result.\n\n```Haskell\n\u003e\u003e\u003e :{\nlet\n  getUsers :: Statement DB () User\n  getUsers = query $ select_\n    (#u ! #name `as` #userName :* #e ! #email `as` #userEmail)\n    ( from (table (#users `as` #u)\n      \u0026 innerJoin (table (#emails `as` #e))\n        (#u ! #id .== #e ! #user_id)) )\n:}\n```\n\n```Haskell\n\u003e\u003e\u003e printSQL getUsers\n```\n```SQL\nSELECT \"u\".\"name\" AS \"userName\", \"e\".\"email\" AS \"userEmail\" FROM \"users\" AS \"u\" INNER JOIN \"emails\" AS \"e\" ON (\"u\".\"id\" = \"e\".\"user_id\")\n```\n\nLet's create some users to add to the database.\n\n```Haskell\n\u003e\u003e\u003e :{\nlet\n  users :: [User]\n  users =\n    [ User \"Alice\" (Just \"alice@gmail.com\")\n    , User \"Bob\" Nothing\n    , User \"Carole\" (Just \"carole@hotmail.com\")\n    ]\n:}\n```\n\nNow we can put together all the pieces into a program. The program\nconnects to the database, sets up the schema, inserts the user data\n(using prepared statements as an optimization), queries the user\ndata and prints it out and finally closes the connection. We can thread\nthe changing schema information through by using the indexed `PQ` monad\ntransformer and when the schema doesn't change we can use `Monad` and\n`MonadPQ` functionality.\n\n```Haskell\n\u003e\u003e\u003e :{\nlet\n  session :: PQ DB DB IO ()\n  session = do\n    executePrepared_ insertUser users\n    usersResult \u003c- execute getUsers\n    usersRows \u003c- getRows usersResult\n    liftIO $ print usersRows\nin\n  withConnection \"host=localhost port=5432 dbname=exampledb user=postgres password=postgres\" $\n    define setup\n    \u0026 pqThen session\n    \u0026 pqThen (define teardown)\n:}\n[User {userName = \"Alice\", userEmail = Just \"alice@gmail.com\"},User {userName = \"Bob\", userEmail = Nothing},User {userName = \"Carole\", userEmail = Just \"carole@hotmail.com\"}]\n```\n\nThis should get you up and running with Squeal. Once you're writing more complicated\nqueries and need a deeper understanding of Squeal's types and how everything\nfits together, check out the [Core Concepts Handbook](squeal-core-concepts-handbook.md).\n","funding_links":[],"categories":["Haskell"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmorphismtech%2Fsqueal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmorphismtech%2Fsqueal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmorphismtech%2Fsqueal/lists"}