diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..807cb6c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @freckle/backenders diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1230149 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/add-asana-comment.yml b/.github/workflows/add-asana-comment.yml new file mode 100644 index 0000000..aaa3f6d --- /dev/null +++ b/.github/workflows/add-asana-comment.yml @@ -0,0 +1,16 @@ +name: Asana + +on: + pull_request: + types: [opened] + +jobs: + link-asana-task: + if: ${{ github.actor != 'dependabot[bot]' }} + runs-on: ubuntu-latest + steps: + - uses: Asana/create-app-attachment-github-action@v1.3 + id: postAttachment + with: + asana-secret: ${{ secrets.ASANA_API_ACCESS_KEY }} + - run: echo "Status is ${{ steps.postAttachment.outputs.status }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6a85d13 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + pull_request: + push: + branches: main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: generate + uses: freckle/stack-action/generate-matrix@v5 + outputs: + stack-yamls: ${{ steps.generate.outputs.stack-yamls }} + fail-fast: false + + test: + needs: generate + runs-on: ubuntu-latest + + strategy: + matrix: + stack-yaml: ${{ fromJSON(needs.generate.outputs.stack-yamls) }} + fail-fast: false + + steps: + - uses: actions/checkout@v4 + - uses: freckle/stack-action@v5 + env: + STACK_YAML: ${{ matrix.stack-yaml }} + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: haskell-actions/hlint-setup@v2 + - uses: haskell-actions/hlint-run@v2 + with: + fail-on: warning diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml new file mode 100644 index 0000000..f1e628a --- /dev/null +++ b/.github/workflows/mergeabot.yml @@ -0,0 +1,19 @@ +name: Mergeabot + +on: + schedule: + - cron: "0 0 * * *" + + pull_request: + +permissions: + contents: write + pull-requests: write + +jobs: + mergeabot: + runs-on: ubuntu-latest + steps: + - uses: freckle/mergeabot-action@v2 + with: + quarantine-days: 5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5d26e2b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release + +on: + push: + branches: main + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - id: tag + uses: freckle/haskell-tag-action@v1 + with: + package-yaml: persistent-sql-lifted/package.yaml + tag-prefix: persistent-sql-lifted-v + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - if: steps.tag.outputs.tag + run: stack upload --pvp-bounds lower persistent-sql-lifted + env: + HACKAGE_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }} + STACK_YAML: stack-lts20.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fda386a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.stack-work +*.hie +.hiedb +stack-*.lock +stack.*.lock +.direnv diff --git a/README.md b/README.md deleted file mode 100644 index c4bf678..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# persistent-sql-lifted -Monad classes for running queries with Persistent and Esqueleto diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4cb23ed --- /dev/null +++ b/flake.lock @@ -0,0 +1,554 @@ +{ + "nodes": { + "autodocodec": { + "flake": false, + "locked": { + "lastModified": 1722952210, + "narHash": "sha256-+vz0En2dkS0nfOq12QAJtV6GcdP0WoDv7HwzrMIsnGo=", + "owner": "NorfairKing", + "repo": "autodocodec", + "rev": "ac36615cb344ec43259fb65cafe3e55308ce4662", + "type": "github" + }, + "original": { + "owner": "NorfairKing", + "repo": "autodocodec", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "freckle": { + "inputs": { + "flake-utils": "flake-utils_2", + "haskell-openapi-code-generator": "haskell-openapi-code-generator", + "nix-github-actions": "nix-github-actions", + "nixpkgs-22-11": "nixpkgs-22-11", + "nixpkgs-23-05": "nixpkgs-23-05", + "nixpkgs-23-11": "nixpkgs-23-11", + "nixpkgs-24-05": "nixpkgs-24-05", + "nixpkgs-24-11": "nixpkgs-24-11", + "nixpkgs-haskell-updates": "nixpkgs-haskell-updates", + "nixpkgs-stable": "nixpkgs-stable_2", + "stack-lint-extra-deps": "stack-lint-extra-deps" + }, + "locked": { + "dir": "main", + "lastModified": 1735950410, + "narHash": "sha256-gDZiLzvhIh5lK7SiSQbXBs+z9mchpwoFFGMwcoxBJYk=", + "ref": "refs/heads/main", + "rev": "a74589ecb93296229dd6a6206f157a6bc73e6883", + "revCount": 122, + "type": "git", + "url": "ssh://git@github.com/freckle/flakes?dir=main" + }, + "original": { + "dir": "main", + "type": "git", + "url": "ssh://git@github.com/freckle/flakes?dir=main" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "freckle", + "haskell-openapi-code-generator", + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "haskell-openapi-code-generator": { + "inputs": { + "autodocodec": "autodocodec", + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks", + "safe-coloured-text": "safe-coloured-text", + "sydtest": "sydtest", + "validity": "validity" + }, + "locked": { + "lastModified": 1731515792, + "narHash": "sha256-EoU807Pqii6hnLvXIu8G2fDOyKLBrOSesXsEEh3YoZQ=", + "owner": "Haskell-OpenAPI-Code-Generator", + "repo": "Haskell-OpenAPI-Client-Code-Generator", + "rev": "0eb15be1a8ae9c311e727e300e7564bf2eb7096f", + "type": "github" + }, + "original": { + "owner": "Haskell-OpenAPI-Code-Generator", + "repo": "Haskell-OpenAPI-Client-Code-Generator", + "type": "github" + } + }, + "nix-github-actions": { + "inputs": { + "nixpkgs": [ + "freckle", + "nixpkgs-stable" + ] + }, + "locked": { + "lastModified": 1731952509, + "narHash": "sha256-p4gB3Rhw8R6Ak4eMl8pqjCPOLCZRqaehZxdZ/mbFClM=", + "owner": "nix-community", + "repo": "nix-github-actions", + "rev": "7b5f051df789b6b20d259924d349a9ba3319b226", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nix-github-actions", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1722987190, + "narHash": "sha256-68hmex5efCiM2aZlAAEcQgmFI4ZwWt8a80vOeB/5w3A=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "21cc704b5e918c5fbf4f9fff22b4ac2681706d90", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-22-11": { + "locked": { + "lastModified": 1688392541, + "narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-23-05": { + "locked": { + "lastModified": 1704290814, + "narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-23-11": { + "locked": { + "lastModified": 1720535198, + "narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-24-05": { + "locked": { + "lastModified": 1735563628, + "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-24-11": { + "locked": { + "lastModified": 1735669367, + "narHash": "sha256-tfYRbFhMOnYaM4ippqqid3BaLOXoFNdImrfBfCp4zn0=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "edf04b75c13c2ac0e54df5ec5c543e300f76f1c9", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-haskell-updates": { + "locked": { + "lastModified": 1735908276, + "narHash": "sha256-U19gzGJFlGtyNonhg3jhhamQUex5ri3MgR1QKz6w/qg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d3780c92e64472e8f9aa54f7bbb0dd4483b98303", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "haskell-updates", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1720386169, + "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable_2": { + "locked": { + "lastModified": 1730883749, + "narHash": "sha256-mwrFF0vElHJP8X3pFCByJR365Q2463ATp2qGIrDUdlE=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "dba414932936fde69f0606b4f1d87c5bc0003ede", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1719082008, + "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9693852a2070b398ee123a329e68f0dab5526681", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1733756863, + "narHash": "sha256-gbivEqdl62NnnESKvRiUR+GRhCGrf1mUoNtGl/yYu4k=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "06886d32c517d0e199c0e6d345d54673acc55453", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "haskell-updates", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1736200483, + "narHash": "sha256-JO+lFN2HsCwSLMUWXHeOad6QUxOuwe9UOAF/iSl1J4I=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "3f0a8ac25fb674611b98089ca3a5dd6480175751", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "pgp-wordlist": { + "flake": false, + "locked": { + "lastModified": 1714149562, + "narHash": "sha256-WCFQgtWqq+gLst4lXkFYlmlW7L8PQOJfChsKMApy3Ng=", + "owner": "quchen", + "repo": "pgp-wordlist", + "rev": "1f0cfd90d62179952cbfd59c3405283a1d364272", + "type": "github" + }, + "original": { + "owner": "quchen", + "repo": "pgp-wordlist", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_2", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1723202784, + "narHash": "sha256-qbhjc/NEGaDbyy0ucycubq4N3//gDFFH3DOmp1D3u1Q=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "c7012d0c18567c889b948781bc74a501e92275d1", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "freckle": "freckle", + "nixpkgs": "nixpkgs_4" + } + }, + "safe-coloured-text": { + "flake": false, + "locked": { + "lastModified": 1722709522, + "narHash": "sha256-zglWqKISYz34A+/PvqwvwrbQYSEcko8C595VAeINitw=", + "owner": "NorfairKing", + "repo": "safe-coloured-text", + "rev": "046f10147a058c00c2706d98c341219e3cbc7669", + "type": "github" + }, + "original": { + "owner": "NorfairKing", + "repo": "safe-coloured-text", + "type": "github" + } + }, + "stack-lint-extra-deps": { + "inputs": { + "nixpkgs": "nixpkgs_3", + "pgp-wordlist": "pgp-wordlist", + "stacklock2nix": "stacklock2nix" + }, + "locked": { + "lastModified": 1734039705, + "narHash": "sha256-fM6/erR1M+QA0XJ09IrncP4e+HbKkkVNBPuzcFirYY0=", + "owner": "freckle", + "repo": "stack-lint-extra-deps", + "rev": "a5a2596ad473fb330b6fa3b89e28f3f1254df0d6", + "type": "github" + }, + "original": { + "owner": "freckle", + "repo": "stack-lint-extra-deps", + "type": "github" + } + }, + "stacklock2nix": { + "locked": { + "lastModified": 1731366632, + "narHash": "sha256-dd10Hyqbyhg83pgxE8Sl1jo9wdYzTU4muH83TjhyrCY=", + "owner": "cdepillabout", + "repo": "stacklock2nix", + "rev": "3cb77e9c869be90ad939f368096ff2cc940881d4", + "type": "github" + }, + "original": { + "owner": "cdepillabout", + "repo": "stacklock2nix", + "type": "github" + } + }, + "sydtest": { + "flake": false, + "locked": { + "lastModified": 1722766036, + "narHash": "sha256-i7N8HKkFPUfnOOmj47Di/XnQS66mUzTG6HISUccNhOA=", + "owner": "NorfairKing", + "repo": "sydtest", + "rev": "23baea50a08857baf3121fc55096c40a93ff5f17", + "type": "github" + }, + "original": { + "owner": "NorfairKing", + "repo": "sydtest", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "validity": { + "flake": false, + "locked": { + "lastModified": 1722943924, + "narHash": "sha256-hq5FwDFW+02u5Qsx8v1KWoQUsY6S6ufI4WYKZ6yhYUA=", + "owner": "NorfairKing", + "repo": "validity", + "rev": "51b8843b9bd5228160b99f653d3271147245d689", + "type": "github" + }, + "original": { + "owner": "NorfairKing", + "repo": "validity", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2e7ec9f --- /dev/null +++ b/flake.nix @@ -0,0 +1,45 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; + freckle.url = "git+ssh://git@github.com/freckle/flakes?dir=main"; + flake-utils.url = "github:numtide/flake-utils"; + }; + outputs = + inputs: + inputs.flake-utils.lib.eachDefaultSystem ( + system: + let + nixpkgs = inputs.nixpkgs.legacyPackages.${system}; + freckle = inputs.freckle.packages.${system}; + freckleLib = inputs.freckle.lib.${system}; + + in + rec { + packages = { + fourmolu = freckle.fourmolu-0-13-x; + ghc = freckleLib.haskellBundle { + ghcVersion = "ghc-9-8-4"; + enableHLS = true; + }; + }; + + devShells.default = nixpkgs.mkShell { + buildInputs = with (nixpkgs); [ + pcre + pcre.dev + zlib + zlib.dev + ]; + + nativeBuildInputs = with (packages); [ + fourmolu + ghc + ]; + + shellHook = '' + export STACK_YAML=stack-lts23.yaml + ''; + }; + } + ); +} diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 0000000..315cc82 --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,17 @@ +indentation: 2 +column-limit: 80 # ignored until v12 / ghc-9.6 +function-arrows: leading +comma-style: leading # default +import-export-style: leading +indent-wheres: false # default +record-brace-space: true +newlines-between-decls: 1 # default +haddock-style: single-line +let-style: mixed +in-style: left-align +single-constraint-parens: never # ignored until v12 / ghc-9.6 +unicode: never # default +respectful: true # default +fixities: + - infixl 1 & + - infixr 4 <>~, .~ diff --git a/hie.yaml b/hie.yaml new file mode 100644 index 0000000..4ef275e --- /dev/null +++ b/hie.yaml @@ -0,0 +1,2 @@ +cradle: + stack: diff --git a/persistent-sql-lifted/CHANGELOG.md b/persistent-sql-lifted/CHANGELOG.md new file mode 100644 index 0000000..fca5443 --- /dev/null +++ b/persistent-sql-lifted/CHANGELOG.md @@ -0,0 +1,9 @@ +## [_Unreleased_](https://github.com/freckle/persistent-sql-lifted/compare/persistent-sql-lifted-v0.1.0.0...main) + +## [v0.0.0.1](https://github.com/freckle/freckle-app/compare/persistent-sql-lifted-v0.0.0.0...persistent-sql-lifted-v0.1.0.0) + +Major expansion, adding query runners for Persistent and Esqueleto. + +## [v0.0.0.0](https://github.com/freckle/freckle-app/tree/persistent-sql-lifted-v0.0.0.0/persistent-sql-lifted) + +First release, sprouted from `freckle-app-1.20.3.0`. diff --git a/LICENSE b/persistent-sql-lifted/LICENSE similarity index 93% rename from LICENSE rename to persistent-sql-lifted/LICENSE index b968161..da6c3e9 100644 --- a/LICENSE +++ b/persistent-sql-lifted/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2025 Freckle +Copyright (c) 2024-2025 Renaissance Learning Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/persistent-sql-lifted/README.md b/persistent-sql-lifted/README.md new file mode 100644 index 0000000..ea77586 --- /dev/null +++ b/persistent-sql-lifted/README.md @@ -0,0 +1,30 @@ +# persistent-sql-lifted + +This package introduces two classes: + +- `MonadSqlBackend m`, for monadic contexts `m` in which a `SqlBackend` is available + +- `MonadSqlBackend db m`, for monadic contexts `m` in which we can execute a SQL + transaction of type `db a` and get a result `m a`. (The type `db` should have an + instance of `MonadSqlBackend`.) + +Additionally, this package provides variants of query-running utilities from +[persistent] and [esqueleto] which are + +1. Concretized to use `SqlBackend`; +2. Generalized to a `MonadSqlBackend m` constraint rather than `ReaderT backend m`; +3. Wrapped in [checkpointCallStack] so that exceptions will include call stacks. + +How to migrate from vanilla persistent/esqueleto: + +- Instead of [SqlPersistT], use a `MonadSqlBackend` constraint. +- Define an instance of `MonadSqlTx` for your application Monad that specifies how + your application runs database transactions, e.g. by running [runSqlPool]. +- Instead of calling `runSqlPool` directly from the rest of your application code, + use the `runSqlTx` method from the `MonadSqlTx` class. + + [checkpointCallStack]: https://hackage.haskell.org/package/annotated-exception-0.3.0.2/docs/Control-Exception-Annotated-UnliftIO.html + [esqueleto]: https://hackage.haskell.org/package/esqueleto + [persistent]: https://hackage.haskell.org/package/persistent + [runSqlPool]: https://hackage.haskell.org/package/persistent-2.14.6.3/docs/Database-Persist-Sql.html#v:runSqlPool + [SqlPersistT]: https://hackage.haskell.org/package/persistent-2.14.6.3/docs/Database-Persist-Sql.html#t:SqlPersistT diff --git a/persistent-sql-lifted/library/Database/Persist/Sql/Lifted.hs b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted.hs new file mode 100644 index 0000000..bcb2046 --- /dev/null +++ b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted.hs @@ -0,0 +1,117 @@ +{-# LANGUAGE CPP #-} + +-- | +-- +-- Re-exports from: +-- +-- * "Database.Persist.Sql.Lifted.Core" +-- * "Database.Persist.Sql.Lifted.Persistent" +-- * "Database.Persist.Sql.Lifted.Esqueleto" +-- +-- There are a few name conflicts between Persistent and Esqueleto. Where conflicts occur, this +-- module gives preference to Esqueleto. The following Persistent definitions are renamed: +-- +-- * 'Database.Persist.Sql.Lifted.Persistent.delete' -> 'deleteKey' +-- * 'Database.Persist.Sql.Lifted.Persistent.update' -> 'update'' +module Database.Persist.Sql.Lifted + ( -- * Core concepts + MonadSqlTx (..) + , HasSqlBackend (..) + , SqlBackend + , MonadSqlBackend (..) + , liftSql + + -- * Getting by key + , get + , getBy + , getByValue + , getEntity + , getJust + , getJustEntity + , getMany + + -- * Selecting by filter + , select + , selectOne + , selectFirst + , selectKeys + , selectKeysList + , selectList + + -- * Selecting counts/existence + , count + , exists + + -- * Inserting + , insertSelect + , insertSelectCount + , insert + , insert_ + , insertBy + , insertEntity + , insertEntityMany + , insertKey + , insertMany + , insertMany_ + , insertRecord + , insertUnique + , insertUniqueEntity + + -- * Updating + , update + , updateCount + , update' + , updateGet + , updateWhere + + -- * Insert/update combinations + , replace + , replaceUnique + , repsert + , repsertMany + , upsert + , upsertBy + , putMany + + -- * Working with unique constraints + , checkUnique + , checkUniqueUpdateable + , onlyUnique + + -- * Deleting + , delete + , deleteKey + , deleteBy + , deleteWhere + , deleteCount + + -- * Rendering queries to text + , renderQueryDelete + , renderQueryInsertInto + , renderQuerySelect + , renderQueryToText + , renderQueryUpdate + ) where + +#if MIN_VERSION_base(4,17,0) +import Data.Type.Equality (type (~)) +#endif +import Database.Persist (Key, PersistEntity (PersistEntityBackend), Update) +import Database.Persist.Sql.Lifted.Core +import Database.Persist.Sql.Lifted.Esqueleto +import Database.Persist.Sql.Lifted.Persistent hiding (delete, update) +import Database.Persist.Sql.Lifted.Persistent qualified as Persistent +import GHC.Stack (HasCallStack) + +-- | Update individual fields on a specific record +update' + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> [Update a] + -> m () +update' = Persistent.update diff --git a/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Core.hs b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Core.hs new file mode 100644 index 0000000..3efd516 --- /dev/null +++ b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Core.hs @@ -0,0 +1,12 @@ +module Database.Persist.Sql.Lifted.Core + ( MonadSqlTx (..) + , HasSqlBackend (..) + , SqlBackend + , MonadSqlBackend (..) + , liftSql + ) where + +import Database.Persist.Sql (SqlBackend) +import Database.Persist.Sql.Lifted.HasSqlBackend +import Database.Persist.Sql.Lifted.MonadSqlBackend +import Database.Persist.Sql.Lifted.MonadSqlTx diff --git a/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Esqueleto.hs b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Esqueleto.hs new file mode 100644 index 0000000..8b6aa05 --- /dev/null +++ b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Esqueleto.hs @@ -0,0 +1,202 @@ +{-# LANGUAGE CPP #-} + +-- | Wrappers that apply 'liftSql' to Esqueleto utilities of the same name. +module Database.Persist.Sql.Lifted.Esqueleto + ( delete + , deleteCount + , deleteKey + , insertSelect + , insertSelectCount + , renderQueryDelete + , renderQueryInsertInto + , renderQuerySelect + , renderQueryToText + , renderQueryUpdate + , select + , selectOne + , update + , updateCount + ) where + +import Data.Function (($)) +import Data.Int (Int64) +import Data.Maybe (Maybe) +import Data.Text (Text) +#if MIN_VERSION_base(4,17,0) +import Data.Type.Equality (type (~)) +#endif +import Database.Esqueleto.Experimental + ( Entity + , PersistEntity (Key, PersistEntityBackend) + , PersistValue + , SqlExpr + , SqlQuery + ) +import Database.Esqueleto.Experimental qualified as E +import Database.Esqueleto.Internal.Internal (Insertion, Mode, SqlSelect) +import Database.Persist.Sql.Lifted.Core (MonadSqlBackend, SqlBackend, liftSql) +import GHC.Stack (HasCallStack) + +-- | Execute an Esqueleto DELETE query +delete :: forall m. (MonadSqlBackend m, HasCallStack) => SqlQuery () -> m () +delete q = liftSql $ E.delete q + +-- | Execute an Esqueleto DELETE query +deleteCount + :: forall m + . (MonadSqlBackend m, HasCallStack) + => SqlQuery () + -> m Int64 + -- ^ The number of rows affected +deleteCount q = liftSql $ E.deleteCount q + +-- | Delete a specific record by identifier +-- +-- Does nothing if record does not exist. +deleteKey + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> m () +deleteKey k = liftSql $ E.deleteKey k + +-- | Insert a 'E.PersistField' for every selected value +insertSelect + :: forall a m + . ( PersistEntity a + , MonadSqlBackend m + , HasCallStack + ) + => SqlQuery (SqlExpr (Insertion a)) + -> m () +insertSelect q = liftSql $ E.insertSelect q + +-- | Insert a 'PersistField' for every selected value, returning the count +insertSelectCount + :: forall a m + . ( PersistEntity a + , MonadSqlBackend m + , HasCallStack + ) + => SqlQuery (SqlExpr (Insertion a)) + -> m Int64 + -- ^ The number of inserted rows +insertSelectCount q = liftSql $ E.insertSelectCount q + +-- | Renders a 'SqlQuery' to 'Text' along with the list of 'PersistValue's +-- that would be supplied to the database for @?@ placeholders +renderQueryDelete + :: forall a r m + . ( SqlSelect a r + , MonadSqlBackend m + , HasCallStack + ) + => SqlQuery a + -- ^ SQL query to render + -> m (Text, [PersistValue]) +renderQueryDelete q = liftSql $ E.renderQueryDelete q + +-- | Renders a 'SqlQuery' to 'Text' along with the list of 'PersistValue's +-- that would be supplied to the database for @?@ placeholders +renderQueryInsertInto + :: forall a r m + . ( SqlSelect a r + , MonadSqlBackend m + , HasCallStack + ) + => SqlQuery a + -- ^ SQL query to render + -> m (Text, [PersistValue]) +renderQueryInsertInto q = liftSql $ E.renderQueryInsertInto q + +-- | Renders a 'SqlQuery' to 'Text' along with the list of 'PersistValue's +-- that would be supplied to the database for @?@ placeholders +renderQuerySelect + :: forall a r m + . ( SqlSelect a r + , MonadSqlBackend m + , HasCallStack + ) + => SqlQuery a + -- ^ SQL query to render + -> m (Text, [PersistValue]) +renderQuerySelect q = liftSql $ E.renderQuerySelect q + +-- | Renders a 'SqlQuery' to 'Text' along with the list of 'PersistValue's +-- that would be supplied to the database for @?@ placeholders +renderQueryToText + :: forall a r m + . ( SqlSelect a r + , MonadSqlBackend m + , HasCallStack + ) + => Mode + -- ^ Whether to render as an SELECT, DELETE, etc. + -- You must ensure that the Mode you pass to this function corresponds + -- with the actual SqlQuery. If you pass a query that uses incompatible + -- features (like an INSERT statement with a SELECT mode) then you'll + -- get a weird result. + -> SqlQuery a + -- ^ SQL query to render + -> m (Text, [PersistValue]) +renderQueryToText m q = liftSql $ E.renderQueryToText m q + +-- | Renders a 'SqlQuery' to 'Text' along with the list of 'PersistValue's +-- that would be supplied to the database for @?@ placeholders +renderQueryUpdate + :: forall a r m + . ( SqlSelect a r + , MonadSqlBackend m + , HasCallStack + ) + => SqlQuery a + -- ^ SQL query to render + -> m (Text, [PersistValue]) +renderQueryUpdate q = liftSql $ E.renderQueryUpdate q + +-- | Execute an Esqueleto SELECT query +select + :: forall a r m + . (SqlSelect a r, MonadSqlBackend m, HasCallStack) + => SqlQuery a + -> m [r] + -- ^ A list of rows +select q = liftSql $ E.select q + +-- | Execute an Esqueleto SELECT query, getting only the first row +selectOne + :: forall a r m + . (SqlSelect a r, MonadSqlBackend m, HasCallStack) + => SqlQuery a + -> m (Maybe r) + -- ^ The first row, or 'Nothing' if no rows are selected +selectOne q = liftSql $ E.selectOne q + +-- | Execute an Esqueleto UPDATE query +update + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => (SqlExpr (Entity a) -> SqlQuery ()) + -> m () +update q = liftSql $ E.update q + +-- | Execute an Esqueleto UPDATE query, returning the count +updateCount + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => (SqlExpr (Entity a) -> SqlQuery ()) + -> m Int64 + -- ^ The number of inserted rows +updateCount q = liftSql $ E.updateCount q diff --git a/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/HasSqlBackend.hs b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/HasSqlBackend.hs new file mode 100644 index 0000000..ac31cd9 --- /dev/null +++ b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/HasSqlBackend.hs @@ -0,0 +1,13 @@ +module Database.Persist.Sql.Lifted.HasSqlBackend + ( HasSqlBackend (..) + ) where + +import Prelude + +import Database.Persist.Sql (SqlBackend) + +class HasSqlBackend a where + getSqlBackend :: a -> SqlBackend + +instance HasSqlBackend SqlBackend where + getSqlBackend = id diff --git a/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/MonadSqlBackend.hs b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/MonadSqlBackend.hs new file mode 100644 index 0000000..7352395 --- /dev/null +++ b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/MonadSqlBackend.hs @@ -0,0 +1,25 @@ +module Database.Persist.Sql.Lifted.MonadSqlBackend + ( MonadSqlBackend (..) + , liftSql + ) where + +import Prelude + +import Control.Exception.Annotated.UnliftIO (checkpointCallStack) +import Control.Monad.IO.Unlift (MonadUnliftIO) +import Control.Monad.Reader (ReaderT (..), asks) +import Database.Persist.Sql (SqlBackend) +import Database.Persist.Sql.Lifted.HasSqlBackend (HasSqlBackend, getSqlBackend) +import GHC.Stack (HasCallStack) + +-- | A monadic context in which a SQL backend is available +-- for running database queries +class MonadUnliftIO m => MonadSqlBackend m where + getSqlBackendM :: m SqlBackend + +instance (HasSqlBackend r, MonadUnliftIO m) => MonadSqlBackend (ReaderT r m) where + getSqlBackendM = asks getSqlBackend + +-- | Generalize from 'SqlPersistT' to 'MonadSqlBackend' +liftSql :: (MonadSqlBackend m, HasCallStack) => ReaderT SqlBackend m a -> m a +liftSql (ReaderT f) = checkpointCallStack $ getSqlBackendM >>= f diff --git a/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/MonadSqlTx.hs b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/MonadSqlTx.hs new file mode 100644 index 0000000..bdcef46 --- /dev/null +++ b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/MonadSqlTx.hs @@ -0,0 +1,15 @@ +module Database.Persist.Sql.Lifted.MonadSqlTx + ( MonadSqlTx (..) + ) where + +import Control.Monad.IO.Unlift (MonadUnliftIO) +import Database.Persist.Sql.Lifted.MonadSqlBackend (MonadSqlBackend) +import GHC.Stack (HasCallStack) + +-- | The constraint @'MonadSqlTx' db m@ indicates that @m@ is a monadic +-- context that can run @db@ actions, usually as a SQL transaction. +-- Typically, this means that @db@ needs a connection and @m@ can +-- provide one, e.g. from a connection pool. +class (MonadSqlBackend db, MonadUnliftIO m) => MonadSqlTx db m | m -> db where + -- | Runs the action in a SQL transaction + runSqlTx :: HasCallStack => db a -> m a diff --git a/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Persistent.hs b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Persistent.hs new file mode 100644 index 0000000..c512cf9 --- /dev/null +++ b/persistent-sql-lifted/library/Database/Persist/Sql/Lifted/Persistent.hs @@ -0,0 +1,643 @@ +{-# LANGUAGE CPP #-} + +-- | Wrappers that apply 'liftSql' to Persistent utilities of the same name. +module Database.Persist.Sql.Lifted.Persistent + ( checkUnique + , checkUniqueUpdateable + , count + , delete + , deleteBy + , deleteWhere + , exists + , get + , getBy + , getByValue + , getEntity + , getJust + , getJustEntity + , getMany + , insert + , insert_ + , insertBy + , insertEntity + , insertEntityMany + , insertKey + , insertMany + , insertMany_ + , insertRecord + , insertUnique + , insertUniqueEntity + , onlyUnique + , putMany + , replace + , replaceUnique + , repsert + , repsertMany + , selectFirst + , selectKeys + , selectKeysList + , selectList + , update + , updateGet + , updateWhere + , upsert + , upsertBy + ) where + +import Conduit (ConduitT, MonadResource, transPipe) +import Data.Bool (Bool) +import Data.Either (Either) +import Data.Eq (Eq) +import Data.Function (($)) +import Data.Int (Int) +import Data.Map.Strict (Map) +import Data.Maybe (Maybe) +#if MIN_VERSION_base(4,17,0) +import Data.Type.Equality (type (~)) +#endif +import Database.Persist + ( AtLeastOneUniqueKey + , Entity + , Filter + , OnlyOneUniqueKey + , PersistEntity (..) + , SelectOpt + , Update + ) +import Database.Persist.Class qualified as P +import Database.Persist.Class.PersistEntity (SafeToInsert) +import Database.Persist.Sql.Lifted.Core (MonadSqlBackend, SqlBackend, liftSql) +import GHC.Stack (HasCallStack) + +-- | Check whether there are any conflicts for unique keys with this entity +-- and existing entities in the database +checkUnique + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m (Maybe (Unique a)) + -- ^ 'Nothing' if the entity would be unique, and could thus safely + -- be inserted. On a conflict, 'Just' the conflicting key. +checkUnique a = liftSql $ P.checkUnique a + +-- | Check whether there are any conflicts for unique keys with this entity and existing entities in the database +-- +-- This is useful for updating because it ignores conflicts when the particular entity already exists. +checkUniqueUpdateable + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Entity a + -> m (Maybe (Unique a)) + -- ^ 'Nothing' if the entity would stay unique, and could thus safely be updated. + -- On a conflict, 'Just' the conflicting key. +checkUniqueUpdateable e = liftSql $ P.checkUniqueUpdateable e + +-- | The total number of records fulfilling the given criteria +count + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Filter a] + -- ^ If you provide multiple values in the list, the conditions are ANDed together. + -> m Int +count fs = liftSql $ P.count fs + +-- | Delete a specific record by identifier +-- +-- Does nothing if record does not exist. +delete + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> m () +delete k = liftSql $ P.delete k + +-- | Delete a specific record by unique key +-- +-- Does nothing if no record matches. +deleteBy + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Unique a + -> m () +deleteBy u = liftSql $ P.deleteBy u + +-- | Delete all records matching the given criteria +deleteWhere + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Filter a] + -- ^ If you provide multiple values in the list, the conditions are ANDed together. + -> m () +deleteWhere fs = liftSql $ P.deleteWhere fs + +-- | Check if there is at least one record fulfilling the given criteria +exists + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Filter a] + -- ^ If you provide multiple values in the list, the conditions are ANDed together. + -> m Bool +exists fs = liftSql $ P.exists fs + +-- | Get a record by identifier, if available +get + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> m (Maybe a) +get k = liftSql $ P.get k + +-- | Get a record by unique key, if available, returning both the identifier and the record +getBy + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Unique a + -> m (Maybe (Entity a)) +getBy u = liftSql $ P.getBy u + +-- Get a record by unique key, if available, returning both the identifier and the record +-- +-- This function makes the most sense on entities with a single 'Unique' constructor. +getByValue + :: forall a m + . ( PersistEntityBackend a ~ SqlBackend + , AtLeastOneUniqueKey a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m (Maybe (Entity a)) + -- ^ A record matching one of the unique keys. +getByValue a = liftSql $ P.getByValue a + +-- | Get a record by identifier, if available +getEntity + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> m (Maybe (Entity a)) +getEntity k = liftSql $ P.getEntity k + +-- | Get a record by identifier, if available, for a non-null (not 'Maybe') foreign key +-- +-- Unsafe unless your database is enforcing that the foreign key is valid. +getJust + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> m a +getJust k = liftSql $ P.getJust k + +-- | Get a record by identifier, if available, for a non-null (not 'Maybe') foreign key +-- +-- Unsafe unless your database is enforcing that the foreign key is valid. +getJustEntity + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> m (Entity a) +getJustEntity k = liftSql $ P.getJustEntity k + +-- | Get many records by their respective identifiers, if available +getMany + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Key a] + -> m (Map (Key a) a) +getMany ks = liftSql $ P.getMany ks + +-- | Create a new record in the database +insert + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m (Key a) + -- ^ The auto-increment ID that was generated +insert a = liftSql $ P.insert a + +-- | Create a new record in the database +insert_ + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m () +insert_ a = liftSql $ P.insert_ a + +-- | Insert a value, checking for conflicts with any unique constraints +insertBy + :: forall a m + . ( PersistEntityBackend a ~ SqlBackend + , AtLeastOneUniqueKey a + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m (Either (Entity a) (Key a)) + -- ^ If a duplicate exists in the database, it is returned as 'Left'. + -- Otherwise, the new 'Key' is returned as 'Right'. +insertBy a = liftSql $ P.insertBy a + +-- | Create a new record in the database, returning an auto-increment ID and the inserted record +insertEntity + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m (Entity a) +insertEntity a = liftSql $ P.insertEntity a + +-- | Create multiple records in the database, with specified keys +insertEntityMany + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Entity a] + -> m () +insertEntityMany es = liftSql $ P.insertEntityMany es + +-- | Create a new record in the database using the given key +insertKey + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> a + -> m () +insertKey k a = liftSql $ P.insertKey k a + +-- | Create multiple records in the database and return their 'Key's +insertMany + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => [a] + -> m [Key a] +insertMany as = liftSql $ P.insertMany as + +-- | Create multiple records in the database +insertMany_ + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => [a] + -> m () +insertMany_ as = liftSql $ P.insertMany_ as + +-- | Create a new record in the database +insertRecord + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m a + -- ^ The record that was inserted +insertRecord a = liftSql $ P.insertRecord a + +-- | Create a new record in the database +insertUnique + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m (Maybe (Key a)) + -- ^ An auto-increment ID, or 'Nothing' when the record couldn't be + -- inserted because of a uniqueness constraint +insertUnique a = liftSql $ P.insertUnique a + +-- | Create a new record in the database +insertUniqueEntity + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m (Maybe (Entity a)) + -- ^ An auto-increment ID and the inserted record, or 'Nothing' when the record + -- couldn't be inserted because of a uniqueness constraint. +insertUniqueEntity a = liftSql $ P.insertUniqueEntity a + +-- | Return the single unique key for a record +onlyUnique + :: forall a m + . ( PersistEntityBackend a ~ SqlBackend + , OnlyOneUniqueKey a + , MonadSqlBackend m + , HasCallStack + ) + => a + -> m (Unique a) +onlyUnique a = liftSql $ P.onlyUnique a + +-- | Put many records into the database +-- +-- * Insert new records that do not exist (or violate any unique constraints); +-- * Replace existing records (matching any unique constraint). +putMany + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => [a] + -- ^ A list of the records you want to insert or replace. + -> m () +putMany as = liftSql $ P.putMany as + +-- | Replace the record in the database with the given key +-- +-- The result is undefined if such record does not exist. +replace + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> a + -> m () +replace k a = liftSql $ P.replace k a + +-- | Attempt to replace the record of the given key with the given new record +-- +-- First query the unique fields to make sure the replacement maintains uniqueness constraints. +replaceUnique + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , Eq (Unique a) + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> a + -> m (Maybe (Unique a)) + -- ^ 'Nothing' if the replacement was made. If uniqueness is violated, + -- 'Just' the 'Unique' violation. +replaceUnique k a = liftSql $ P.replaceUnique k a + +-- | Put the record in the database with the given key +-- +-- If a record with the given key does not exist then a new record will be inserted. +repsert + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> a + -> m () +repsert k a = liftSql $ P.repsert k a + +-- | Put many entities into the database +-- +-- For each item, if a record with the given key does not exist then a new record will be inserted. +repsertMany + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [(Key a, a)] + -> m () +repsertMany kas = liftSql $ P.repsertMany kas + +-- | Get just the first record for the criteria +selectFirst + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Filter a] + -- ^ If you provide multiple values in the list, the conditions are ANDed together. + -> [SelectOpt a] + -> m (Maybe (Entity a)) +selectFirst fs os = liftSql $ P.selectFirst fs os + +-- | Get the 'Key's of all records matching the given criteria +selectKeys + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , MonadResource m + , HasCallStack + ) + => [Filter a] + -- ^ If you provide multiple values in the list, the conditions are ANDed together. + -> [SelectOpt a] + -> ConduitT () (Key a) m () + -- ^ Keys corresponding to the filters and options provided +selectKeys fs os = transPipe liftSql $ P.selectKeys fs os + +-- | Get the 'Key's of all records matching the given criteria +selectKeysList + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Filter a] + -- ^ If you provide multiple values in the list, the conditions are ANDed together. + -> [SelectOpt a] + -> m [Key a] +selectKeysList fs os = liftSql $ P.selectKeysList fs os + +selectList + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Filter a] + -- ^ If you provide multiple values in the list, the conditions are ANDed together. + -> [SelectOpt a] + -> m [Entity a] + -- ^ Entities corresponding to the filters and options provided +selectList fs os = liftSql $ P.selectList fs os + +-- | Update individual fields on a specific record +update + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> [Update a] + -> m () +update k us = liftSql $ P.update k us + +-- | Update individual fields on a specific record, and retrieve the updated value from the database +-- +-- This function will throw an exception if the given key is not found in the database. +updateGet + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => Key a + -> [Update a] + -> m a +updateGet k us = liftSql $ P.updateGet k us + +-- | Update individual fields on any record matching the given criteria +updateWhere + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , MonadSqlBackend m + , HasCallStack + ) + => [Filter a] + -- ^ If you provide multiple values in the list, the conditions are ANDed together. + -> [Update a] + -> m () +updateWhere fs us = liftSql $ P.updateWhere fs us + +-- | Update based on a uniqueness constraint or insert: +-- +-- * Unsert the new record if it does not exist; +-- * If the record exists (matched via it's uniqueness constraint), then update the +-- existing record with the parameters which is passed on as list to the function. +upsert + :: forall a m + . ( PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , OnlyOneUniqueKey a + , MonadSqlBackend m + , HasCallStack + ) + => a + -- ^ New record to insert + -> [Update a] + -- ^ Updates to perform if the record already exists + -> m (Entity a) + -- ^ The record in the database after the operation +upsert a us = liftSql $ P.upsert a us + +-- | Update based on a given uniqueness constraint or insert: +-- +-- * Insert the new record if it does not exist; +-- * Update the existing record that matches the given uniqueness constraint. +upsertBy + :: forall a m + . ( PersistEntity a + , PersistEntityBackend a ~ SqlBackend + , SafeToInsert a + , MonadSqlBackend m + , HasCallStack + ) + => Unique a + -- ^ Uniqueness constraint to find by + -> a + -- ^ New record to insert + -> [Update a] + -- ^ Updates to perform if the record already exists + -> m (Entity a) + -- ^ The record in the database after the operation +upsertBy u a us = liftSql $ P.upsertBy u a us diff --git a/persistent-sql-lifted/package.yaml b/persistent-sql-lifted/package.yaml new file mode 100644 index 0000000..9adecb2 --- /dev/null +++ b/persistent-sql-lifted/package.yaml @@ -0,0 +1,68 @@ +name: persistent-sql-lifted +version: 0.1.0.0 + +maintainer: Freckle Education +category: Database +github: freckle/persistent-sql-lifted +synopsis: Monad classes for running queries with Persistent and Esqueleto +description: | + This package introduces two classes: MonadSqlBackend for monadic contexts in + which a SqlBackend is available, and MonadSqlTx for contexts in which we + can execute a SQL transaction. + + Additionally, this package provides variants of query-running utilities from + Persistent and Esqueleto which are concretized to use SqlBackend, generalized + to a MonadSqlBackend m constraint rather than "ReaderT backend", and wrapped in + checkpointCallStack so that exceptions will include call stacks. + +extra-doc-files: + - README.md + - CHANGELOG.md + +extra-source-files: + - package.yaml + +language: GHC2021 + +ghc-options: + - -fignore-optim-changes + - -fwrite-ide-info + - -Weverything + - -Wno-all-missed-specialisations + - -Wno-missing-exported-signatures # re-enables missing-signatures + - -Wno-missing-import-lists + - -Wno-missing-kind-signatures + - -Wno-missing-local-signatures + - -Wno-missing-safe-haskell-mode + - -Wno-monomorphism-restriction + - -Wno-prepositive-qualified-module + - -Wno-safe + - -Wno-unsafe + +when: + - condition: "impl(ghc >= 9.8)" + ghc-options: + - -Wno-missing-role-annotations + - -Wno-missing-poly-kind-signatures + +dependencies: + - base < 5 + +default-extensions: + - ExplicitNamespaces + - FunctionalDependencies + - GADTs + - NoImplicitPrelude + - NoMonomorphismRestriction + +library: + source-dirs: library + dependencies: + - annotated-exception + - conduit + - containers + - esqueleto + - mtl + - persistent + - text + - unliftio-core diff --git a/persistent-sql-lifted/persistent-sql-lifted.cabal b/persistent-sql-lifted/persistent-sql-lifted.cabal new file mode 100644 index 0000000..4d0fd4f --- /dev/null +++ b/persistent-sql-lifted/persistent-sql-lifted.cabal @@ -0,0 +1,67 @@ +cabal-version: 1.18 + +-- This file has been generated from package.yaml by hpack version 0.37.0. +-- +-- see: https://github.com/sol/hpack + +name: persistent-sql-lifted +version: 0.1.0.0 +synopsis: Monad classes for running queries with Persistent and Esqueleto +description: This package introduces two classes: MonadSqlBackend for monadic contexts in + which a SqlBackend is available, and MonadSqlBackend for contexts in which we + can execute a SQL transaction. + . + Additionally, this package provides variants of query-running utilities from + Persistent and Esqueleto which are concretized to use SqlBackend, generalized + to a MonadSqlBackend m constraint rather than "ReaderT backend", and wrapped in + checkpointCallStack so that exceptions will include call stacks. +category: Database +homepage: https://github.com/freckle/persistent-sql-lifted#readme +bug-reports: https://github.com/freckle/persistent-sql-lifted/issues +maintainer: Freckle Education +license: MIT +license-file: LICENSE +build-type: Simple +extra-source-files: + package.yaml +extra-doc-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/freckle/persistent-sql-lifted + +library + exposed-modules: + Database.Persist.Sql.Lifted + Database.Persist.Sql.Lifted.Core + Database.Persist.Sql.Lifted.Esqueleto + Database.Persist.Sql.Lifted.HasSqlBackend + Database.Persist.Sql.Lifted.MonadSqlBackend + Database.Persist.Sql.Lifted.MonadSqlTx + Database.Persist.Sql.Lifted.Persistent + other-modules: + Paths_persistent_sql_lifted + hs-source-dirs: + library + default-extensions: + ExplicitNamespaces + FunctionalDependencies + GADTs + NoImplicitPrelude + NoMonomorphismRestriction + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-safe -Wno-unsafe + build-depends: + annotated-exception + , base <5 + , conduit + , containers + , esqueleto + , mtl + , persistent + , text + , unliftio-core + default-language: GHC2021 + if impl(ghc >= 9.8) + ghc-options: -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures diff --git a/stack-lts20.yaml b/stack-lts20.yaml new file mode 100644 index 0000000..f0a8c83 --- /dev/null +++ b/stack-lts20.yaml @@ -0,0 +1,7 @@ +resolver: lts-20.26 + +packages: + - persistent-sql-lifted + +extra-deps: + - persistent-2.14.0.1 diff --git a/stack-lts21.yaml b/stack-lts21.yaml new file mode 100644 index 0000000..b797f81 --- /dev/null +++ b/stack-lts21.yaml @@ -0,0 +1,4 @@ +resolver: lts-21.25 + +packages: + - persistent-sql-lifted diff --git a/stack-lts22.yaml b/stack-lts22.yaml new file mode 100644 index 0000000..68fd254 --- /dev/null +++ b/stack-lts22.yaml @@ -0,0 +1,4 @@ +resolver: lts-22.43 + +packages: + - persistent-sql-lifted diff --git a/stack-lts23.yaml b/stack-lts23.yaml new file mode 100644 index 0000000..c52cfc2 --- /dev/null +++ b/stack-lts23.yaml @@ -0,0 +1,4 @@ +resolver: lts-23.3 + +packages: + - persistent-sql-lifted diff --git a/stack-nightly.yaml b/stack-nightly.yaml new file mode 100644 index 0000000..142730f --- /dev/null +++ b/stack-nightly.yaml @@ -0,0 +1,4 @@ +resolver: nightly-2025-01-06 + +packages: + - persistent-sql-lifted