├── .github
├── FUNDING.yml
└── workflows
│ └── CI.yml
├── .gitignore
├── .tidyrc.json
├── LICENSE
├── PURESCRIPT-POSTGRESQL-CLIENT-LICENSE
├── README.md
├── bower.json
├── docs
├── CNAME
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── _layouts
│ └── default.html
├── assets
│ └── css
│ │ └── site.css
├── edsl.md
├── favicon.ico
├── index.md
├── mapper.md
└── migrations.md
├── package-lock.json
├── package.json
├── packages.dhall
├── spago.dhall
├── src
└── Droplet
│ ├── Driver.purs
│ ├── Driver
│ ├── Internal
│ │ ├── Migration.purs
│ │ ├── Pool.js
│ │ ├── Pool.purs
│ │ ├── Query.js
│ │ └── Query.purs
│ ├── Migration.purs
│ └── Unsafe.purs
│ ├── Language.purs
│ └── Language
│ └── Internal
│ ├── Condition.purs
│ ├── Definition.js
│ ├── Definition.purs
│ ├── Function.purs
│ ├── Syntax.purs
│ ├── Token.purs
│ └── Translate.purs
└── test
├── Alter.purs
├── As.purs
├── Create.purs
├── Delete.purs
├── Distinct.purs
├── Drop.purs
├── From.purs
├── Function.purs
├── GroupBy.purs
├── Insert.purs
├── Join.purs
├── Limit.purs
├── Main.purs
├── Migration.purs
├── Model.purs
├── NakedSelect.purs
├── Offset.purs
├── OrderBy.purs
├── Returning.purs
├── SubQuery.purs
├── Transaction.purs
├── Types.purs
├── Union.purs
├── Unsafe.purs
├── Update.purs
├── Where.purs
└── sql
└── index.sql
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: easafe
4 | custom: ["https://asafe.dev/donate"]
5 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: push
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | services:
9 | # Label used to access the service container
10 | postgres:
11 | # Docker Hub image
12 | image: postgres
13 | # Provide the password for postgres
14 | env:
15 | POSTGRES_USER: droplet
16 | POSTGRES_PASSWORD: droplet
17 | # Set health checks to wait until postgres has started
18 | options: >-
19 | --health-cmd pg_isready
20 | --health-interval 10s
21 | --health-timeout 5s
22 | --health-retries 5
23 | ports:
24 | # Maps tcp port 5432 on service container to the host
25 | - 5432:5432
26 |
27 | steps:
28 | - uses: actions/checkout@v2
29 | - uses: actions/setup-node@v2
30 | with:
31 | node-version: '14'
32 | cache: 'npm'
33 |
34 | - uses: purescript-contrib/setup-purescript@main
35 |
36 | - name: Cache PureScript dependencies
37 | uses: actions/cache@v2
38 | # This cache uses the .dhall files to know when it should reinstall
39 | # and rebuild packages. It caches both the installed packages from
40 | # the `.spago` directory and compilation artifacts from the `output`
41 | # directory. When restored the compiler will rebuild any files that
42 | # have changed. If you do not want to cache compiled output, remove
43 | # the `output` path.
44 | with:
45 | key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }}
46 | path: |
47 | .spago
48 | output
49 | - run: psql -f test/sql/index.sql postgresql://droplet:droplet@localhost:5432/droplet
50 | - run: npm install
51 | - run: spago build
52 | - run: spago test --no-install
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | .psci_modules
3 | bower_components
4 | node_modules
5 |
6 | # Generated files
7 | .psci
8 | output
9 | .psc-ide-port
10 | .purs-repl
11 | .spago
12 | Scratch.*
13 |
14 | Invalid.*
15 | Example.purs
16 |
17 | todo
18 |
19 | _site/
20 | vendor/
21 | .bundle/
22 | .vscode
--------------------------------------------------------------------------------
/.tidyrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "importWrap": "source",
3 | "indent": 6,
4 | "operatorsFile": null,
5 | "ribbon": 1,
6 | "typeArrowPlacement": "last",
7 | "unicode": "always",
8 | "width": null
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Eduardo Asafe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PURESCRIPT-POSTGRESQL-CLIENT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017, rightfold
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of the copyright holder nor the names of its contributors
15 | may be used to endorse or promote products derived from this software without
16 | specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## purescript-droplet 
2 |
3 | Composable, type-safe eDSL and query mapper for PureScript targeting PostgreSQL
4 |
5 | * eDSL made out of combinators (almost) identical to their SQL counterparts
6 |
7 | * Generated SQL matches eDSL one-to-one, and is guaranteed to be correct and unambiguous
8 |
9 | * Supports nearly all common SQL operations for ```SELECT```, ```INSERT```, ```UPDATE```, ``DELETE`` and others, including joins, aggregations, subqueries, etc
10 |
11 | * Very little boilerplate: query results are automatically mapped to records, (valid) queries never require type annotations
12 |
13 | * Migration support via [pebble](https://github.com/easafe/haskell-pebble)
14 |
15 | ### Documentation
16 |
17 | See the [project page](https://droplet.asafe.dev/) for an in-depth look, or [pursuit](https://pursuit.purescript.org/packages/purescript-droplet) for API docs
18 |
19 | ### Quick start
20 |
21 | Install
22 |
23 | ```
24 | npm i big-integer pg @easafe/pebble
25 |
26 | spago install droplet
27 | ```
28 |
29 | Create some tables
30 |
31 | ```sql
32 | create table users (
33 | id integer generated always as identity primary key,
34 | name text not null,
35 | birthday date null
36 | );
37 |
38 | create table messages (
39 | id integer generated always as identity primary key,
40 | sender integer not null,
41 | recipient integer not null,
42 | date timestamptz default now(),
43 |
44 | constraint sender_user foreign key (sender) references users(id),
45 | constraint recipient_user foreign key (recipient) references users(id)
46 | );
47 | ```
48 |
49 | Define some types for your SQL
50 |
51 | ```purescript
52 | -- representation of the table itself
53 | type UsersTable = Table "users" Users
54 |
55 | users :: UsersTable
56 | users = Table
57 |
58 | messages :: Table "messages" Messages
59 | messages = Table
60 |
61 | -- representation of the table's columns definitions
62 | type Users = (
63 | id :: Column Int (PrimaryKey /\ Identity), -- primary key, generated always as identity
64 | name :: String,
65 | birthday :: Maybe Date -- nullable column
66 | )
67 |
68 | type Messages = (
69 | id :: Column Int (PrimaryKey /\ Identity),
70 | sender :: Column Int (ForeignKey "id" UsersTable), -- foreign key to table users
71 | recipient :: Column Int (ForeignKey "id" UsersTable),
72 | date :: Column DateTime (Constraint "date_default_messages" Default) -- column with named default constrain
73 | )
74 |
75 | -- representation of column names to be used in queries
76 | id :: Proxy "id"
77 | id = Proxy
78 |
79 | name :: Proxy "name"
80 | name = Proxy
81 |
82 | birthday :: Proxy "birthday"
83 | birthday = Proxy
84 |
85 | sender :: Proxy "sender"
86 | sender = Proxy
87 |
88 | recipient :: Proxy "recipient"
89 | recipient = Proxy
90 |
91 | date :: Proxy "date"
92 | date = Proxy
93 |
94 | -- alias
95 | u :: Proxy "u"
96 | u = Proxy
97 |
98 | m :: Proxy "m"
99 | m = Proxy
100 |
101 | t :: Proxy "t"
102 | t = Proxy
103 | ```
104 |
105 | (Don't worry, table creation, typing and migration can be automated with [pebble](https://github.com/easafe/haskell-pebble))
106 |
107 | Prepare some queries
108 |
109 | ```purescript
110 | mary :: _
111 | mary =
112 | insert #
113 | into users (name) #
114 | values ("Mary Sue") # -- `name` is the only required field; it would be a type error to set `id`, as it is an identity column
115 | returning id -- output inserted `id`
116 |
117 | gary :: Date -> _
118 | gary bday =
119 | insert #
120 | into users (name /\ birthday) # -- tuple for field list
121 | values ("Gary Stu" /\ Just bday) # -- set the nullable field `birthday`
122 | returning id
123 |
124 | chat :: Int -> Int -> _
125 | chat from to = insert # into messages (sender /\ recipient) # values (from /\ to) -- `date` has a default value
126 |
127 | selectMessages :: _
128 | selectMessages =
129 | select (id /\ date) #
130 | from messages
131 |
132 | selectUserMessages :: Int -> _
133 | selectUserMessages userId =
134 | selectMessages #
135 | wher (id .=. userId) -- SQL operators are surrounded by dots; we can compare `id` to `userId` as `Column` type wrappers are automatically stripped
136 |
137 | joinUserMessages :: _
138 | joinUserMessages =
139 | select (u ... name /\ -- `alias ... column` is equivalent to SQL alias.column
140 | (t ... name # as recipient) /\ -- `name` is displayed as recipient
141 | date) #
142 | from (((messages # as m)
143 | `join`
144 | (users # as u) #
145 | on (m ... sender .=. u ... id))
146 | `join`
147 | (users # as t) #
148 | on (m ... recipient .=. t ... id))
149 | ```
150 |
151 | Connect to the database
152 |
153 | ```purescript
154 | connectionInfo :: Configuration
155 | connectionInfo = (Driver.defaultConfiguration "database") {
156 | user = Just "user"
157 | }
158 |
159 | example :: Aff Unit
160 | example = do
161 | pool <- liftEffect $ Pool.newPool connectionInfo -- connection pool from PostgreSQL
162 | Driver.withConnection pool case _ of
163 | Left error -> pure unit -- or some more sensible handling
164 | Right connection -> runSql connection
165 | ```
166 |
167 | Run queries
168 |
169 | ```purescript
170 | runSql :: Connection -> Aff Unit
171 | runSql connection = do
172 | now <- liftEffect Now.nowDate
173 | mRow <- Driver.single connection mary -- run a query that returns a single row
174 | gRow <- Driver.single connection $ gary now
175 | case mRow, gRow of
176 | Right (Just {id: mId}), Right (Just {id: gId}) -> void do
177 | mErr <- Driver.execute connection $ chat mId gId -- run a query that doesn't produce an output
178 | gErr <- Driver.execute connection $ chat gId mId
179 |
180 | mMessages <- Driver.query connection $ selectUserMessages mId -- run a query that returns rows
181 | gMessages <- Driver.query connection $ selectUserMessages gId -- rows are always records, the keys are the projected columns
182 | Driver.query connection joinUserMessages
183 |
184 | _, _ -> pure unit
185 | ```
186 |
187 | ### Licensing
188 |
189 | Wrapper code around [pg](https://github.com/brianc/node-postgres) was adapted from [purescript-postgresql-client](https://github.com/rightfold/purescript-postgresql-client) so its license has been included in [PURESCRIPT-POSTGRESQL-CLIENT-LICENSE](PURESCRIPT-POSTGRESQL-CLIENT-LICENSE)
190 |
191 | ### Funding
192 |
193 | If this project is useful for you, consider [throwing a buck](https://asafe.dev/donate) to keep development possible
194 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "purescript-droplet",
3 | "license": [
4 | "MIT"
5 | ],
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/easafe/purescript-droplet"
9 | },
10 | "ignore": [
11 | "**/.*",
12 | "node_modules",
13 | "bower_components",
14 | "output"
15 | ],
16 | "dependencies": {
17 | "purescript-aff": "^v7.0.0",
18 | "purescript-arrays": "^v7.0.0",
19 | "purescript-bifunctors": "^v6.0.0",
20 | "purescript-bigints": "^v7.0.1",
21 | "purescript-datetime": "^v6.0.0",
22 | "purescript-debug": "^v6.0.0",
23 | "purescript-effect": "^v4.0.0",
24 | "purescript-either": "^v6.0.0",
25 | "purescript-enums": "^v6.0.0",
26 | "purescript-exceptions": "^v6.0.0",
27 | "purescript-foldable-traversable": "^v6.0.0",
28 | "purescript-foreign": "^v7.0.0",
29 | "purescript-foreign-object": "^v4.0.0",
30 | "purescript-integers": "^v6.0.0",
31 | "purescript-maybe": "^v6.0.0",
32 | "purescript-newtype": "^v5.0.0",
33 | "purescript-nonempty": "^v7.0.0",
34 | "purescript-nullable": "^v6.0.0",
35 | "purescript-ordered-collections": "^v3.0.0",
36 | "purescript-partial": "^v4.0.0",
37 | "purescript-prelude": "^v6.0.0",
38 | "purescript-profunctor": "^v6.0.0",
39 | "purescript-record": "^v4.0.0",
40 | "purescript-spec": "^v7.0.0",
41 | "purescript-strings": "^v6.0.0",
42 | "purescript-transformers": "^v6.0.0",
43 | "purescript-tuples": "^v7.0.0",
44 | "purescript-type-equality": "^v4.0.1",
45 | "purescript-typelevel-prelude": "^v7.0.0",
46 | "purescript-unsafe-coerce": "^v6.0.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | droplet.asafe.dev
2 |
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Hello! This is where you manage which Jekyll version is used to run.
4 | # When you want to use a different version, change it below, save the
5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
6 | #
7 | # bundle exec jekyll serve
8 | #
9 | # This will help ensure the proper Jekyll version is running.
10 | # Happy Jekylling!
11 | gem "jekyll", "~> 3.8.5"
12 |
13 | # This is the default theme for new Jekyll sites. You may change this to anything you like.
14 | gem "minima", "~> 2.0"
15 |
16 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
17 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
18 | # gem "github-pages", group: :jekyll_plugins
19 |
20 | # If you have any plugins, put them here!
21 | group :jekyll_plugins do
22 | gem "jekyll-feed", "~> 0.6"
23 | end
24 |
25 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
26 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
27 |
28 | # Performance-booster for watching directories on Windows
29 | gem "wdm", "~> 0.1.0" if Gem.win_platform?
30 |
31 |
--------------------------------------------------------------------------------
/docs/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | addressable (2.8.0)
5 | public_suffix (>= 2.0.2, < 5.0)
6 | colorator (1.1.0)
7 | concurrent-ruby (1.1.8)
8 | em-websocket (0.5.2)
9 | eventmachine (>= 0.12.9)
10 | http_parser.rb (~> 0.6.0)
11 | eventmachine (1.2.7)
12 | ffi (1.15.0)
13 | forwardable-extended (2.6.0)
14 | http_parser.rb (0.6.0)
15 | i18n (0.9.5)
16 | concurrent-ruby (~> 1.0)
17 | jekyll (3.8.7)
18 | addressable (~> 2.4)
19 | colorator (~> 1.0)
20 | em-websocket (~> 0.5)
21 | i18n (~> 0.7)
22 | jekyll-sass-converter (~> 1.0)
23 | jekyll-watch (~> 2.0)
24 | kramdown (~> 1.14)
25 | liquid (~> 4.0)
26 | mercenary (~> 0.3.3)
27 | pathutil (~> 0.9)
28 | rouge (>= 1.7, < 4)
29 | safe_yaml (~> 1.0)
30 | jekyll-feed (0.15.1)
31 | jekyll (>= 3.7, < 5.0)
32 | jekyll-sass-converter (1.5.2)
33 | sass (~> 3.4)
34 | jekyll-seo-tag (2.7.1)
35 | jekyll (>= 3.8, < 5.0)
36 | jekyll-watch (2.2.1)
37 | listen (~> 3.0)
38 | kramdown (1.17.0)
39 | liquid (4.0.3)
40 | listen (3.5.1)
41 | rb-fsevent (~> 0.10, >= 0.10.3)
42 | rb-inotify (~> 0.9, >= 0.9.10)
43 | mercenary (0.3.6)
44 | minima (2.5.1)
45 | jekyll (>= 3.5, < 5.0)
46 | jekyll-feed (~> 0.9)
47 | jekyll-seo-tag (~> 2.1)
48 | pathutil (0.16.2)
49 | forwardable-extended (~> 2.6)
50 | public_suffix (4.0.6)
51 | rb-fsevent (0.11.0)
52 | rb-inotify (0.10.1)
53 | ffi (~> 1.0)
54 | rouge (3.26.0)
55 | safe_yaml (1.0.5)
56 | sass (3.7.4)
57 | sass-listen (~> 4.0.0)
58 | sass-listen (4.0.0)
59 | rb-fsevent (~> 0.9, >= 0.9.4)
60 | rb-inotify (~> 0.9, >= 0.9.7)
61 |
62 | PLATFORMS
63 | ruby
64 |
65 | DEPENDENCIES
66 | jekyll (~> 3.8.5)
67 | jekyll-feed (~> 0.6)
68 | minima (~> 2.0)
69 | tzinfo-data
70 |
71 | BUNDLED WITH
72 | 2.1.4
73 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 |
11 | # Site settings
12 | # These are used to personalize your new site. If you look in the HTML files,
13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
14 | # You can create any custom variable you would like, and they will be accessible
15 | # in the templates via {{ site.myvariable }}.
16 |
17 | # Build settings
18 | markdown: kramdown
19 | highlighter: rouge
20 |
21 | # Exclude from processing.
22 | # The following items will not be processed, by default. Create a custom list
23 | # to override the default setting.
24 | # exclude:
25 | # - Gemfile
26 | # - Gemfile.lock
27 | # - node_modules
28 | # - vendor/bundle/
29 | # - vendor/cache/
30 | # - vendor/gems/
31 | # - vendor/ruby/
32 |
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Droplet - {{ page.title }}
7 |
8 |
9 |
10 |
11 |
25 |
26 |
27 | {{ content }}
28 |
29 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/docs/assets/css/site.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans');
2 | @import url('https://fonts.googleapis.com/css?family=Catamaran:400,700');
3 | html {
4 | height: 100%;
5 | box-sizing: border-box;
6 | background-color: #212120;
7 | color:rgb(238, 238, 238);
8 | }
9 |
10 | * {
11 | background: inherit
12 | }
13 |
14 | body {
15 | margin: 0;
16 | min-height: 100%;
17 | font-family: "Open sans", "sans serif";
18 | font-size: 18px;
19 | line-height: 1.51857143;
20 | display: flex;
21 | }
22 |
23 | h1, h2, h3, h4 {
24 | -webkit-margin-before: 0.0em;
25 | -webkit-margin-after: 0.0em;
26 | font-weight: bold;
27 | color: #3575cc;
28 | font-family: "Catamaran";
29 | }
30 |
31 |
32 | a {
33 | text-decoration: none;
34 | font-weight: bold;
35 | }
36 |
37 | a:visited {
38 | text-decoration: none
39 | }
40 |
41 | h1 {
42 | font-size: 2em;
43 | }
44 |
45 | h2 {
46 | font-size: 1.6em;
47 | }
48 |
49 | h3 {
50 | font-size: 1.3em;
51 | }
52 |
53 | ul {
54 | padding: 0;
55 | }
56 |
57 | ul li {
58 | list-style-type: none;
59 | padding-bottom: 14px;
60 | }
61 |
62 | .menu {
63 | padding: 40px;
64 | padding-right: 60px;
65 | display: flex;
66 | position: sticky;
67 | top: 10px;
68 | z-index: 1;
69 | flex-flow: column;
70 | }
71 |
72 | .content {
73 | padding: 60px;
74 | width: 1024px;
75 | }
76 |
77 | .content a {
78 | font-family: "Open sans", "sans serif";
79 | }
80 |
81 | a.direction, .menu a:not(.project-name) {
82 | color: #212120;
83 | background-color: #3575cc;
84 | padding: 3px 5px;
85 | font-family: "Catamaran";
86 | display: inline-block;
87 | border-radius: 2px;
88 | font-size: 1.1em;
89 | }
90 |
91 | .content a:not(.direction), .menu a.current, .menu a:hover {
92 | color: #3575cc;
93 | background-color: #212120;
94 | }
95 |
96 | .content ul li {
97 | list-style-type: circle;
98 | padding: 5px
99 | }
100 |
101 | .project-name {
102 | color: #3575cc;
103 | background-color: #212120;
104 | font-size: 1.3em;
105 | padding: 0;
106 | }
107 |
108 | .previous {
109 | display: inline-block;
110 | margin-right: 20px;
111 | margin-bottom: 20px;
112 | }
113 |
114 | .current {
115 | border-bottom: 3px solid #3575cc;
116 | }
117 |
118 | @media (max-width:600px) {
119 | .menu, .content {
120 | max-width: 100%;
121 | padding: 5px 10px;
122 | }
123 | body {
124 | display: block;
125 | }
126 | div {
127 | width: 100%;
128 | display: block;
129 | }
130 |
131 | p {
132 | display: block;
133 | word-wrap: break-word;
134 | max-width: 97vw;
135 |
136 | }
137 |
138 | }
139 |
140 | .highlight .c {
141 | color: #75715e
142 | }
143 |
144 | /* Comment */
145 |
146 | .highlight .err {
147 | color: #960050;
148 | background-color: #1e0010
149 | }
150 |
151 | /* Error */
152 |
153 | .highlight .k {
154 | color: #66d9ef
155 | }
156 |
157 | /* Keyword */
158 |
159 | .highlight .l {
160 | color: #ae81ff
161 | }
162 |
163 | /* Literal */
164 |
165 | .highlight .n {
166 | color: #f8f8f2
167 | }
168 |
169 | /* Name */
170 |
171 | .highlight .o {
172 | color: #f92672
173 | }
174 |
175 | /* BinaryOperator */
176 |
177 | .highlight .p {
178 | color: #f8f8f2
179 | }
180 |
181 | /* Punctuation */
182 |
183 | .highlight .cm {
184 | color: #75715e
185 | }
186 |
187 | /* Comment.Multiline */
188 |
189 | .highlight .cp {
190 | color: #75715e
191 | }
192 |
193 | /* Comment.Preproc */
194 |
195 | .highlight .c1 {
196 | color: #75715e
197 | }
198 |
199 | /* Comment.Single */
200 |
201 | .highlight .cs {
202 | color: #75715e
203 | }
204 |
205 | /* Comment.Special */
206 |
207 | .highlight .ge {
208 | font-style: italic
209 | }
210 |
211 | /* Generic.Emph */
212 |
213 | .highlight .gs {
214 | font-weight: bold
215 | }
216 |
217 | /* Generic.Strong */
218 |
219 | .highlight .kc {
220 | color: #66d9ef
221 | }
222 |
223 | /* Keyword.Constant */
224 |
225 | .highlight .kd {
226 | color: #66d9ef
227 | }
228 |
229 | /* Keyword.Declaration */
230 |
231 | .highlight .kn {
232 | color: #f92672
233 | }
234 |
235 | /* Keyword.Namespace */
236 |
237 | .highlight .kp {
238 | color: #66d9ef
239 | }
240 |
241 | /* Keyword.Pseudo */
242 |
243 | .highlight .kr {
244 | color: #66d9ef
245 | }
246 |
247 | /* Keyword.Reserved */
248 |
249 | .highlight .kt {
250 | color: #66d9ef
251 | }
252 |
253 | /* Keyword.Type */
254 |
255 | .highlight .ld {
256 | color: #e6db74
257 | }
258 |
259 | /* Literal.Date */
260 |
261 | .highlight .m {
262 | color: #ae81ff
263 | }
264 |
265 | /* Literal.Number */
266 |
267 | .highlight .s {
268 | color: #e6db74
269 | }
270 |
271 | /* Literal.String */
272 |
273 | .highlight .na {
274 | color: #a6e22e
275 | }
276 |
277 | /* Name.Attribute */
278 |
279 | .highlight .nb {
280 | color: #f8f8f2
281 | }
282 |
283 | /* Name.Builtin */
284 |
285 | .highlight .nc {
286 | color: #a6e22e
287 | }
288 |
289 | /* Name.Class */
290 |
291 | .highlight .no {
292 | color: #66d9ef
293 | }
294 |
295 | /* Name.Constant */
296 |
297 | .highlight .nd {
298 | color: #a6e22e
299 | }
300 |
301 | /* Name.Decorator */
302 |
303 | .highlight .ni {
304 | color: #f8f8f2
305 | }
306 |
307 | /* Name.Entity */
308 |
309 | .highlight .ne {
310 | color: #a6e22e
311 | }
312 |
313 | /* Name.Exception */
314 |
315 | .highlight .nf {
316 | color: #a6e22e
317 | }
318 |
319 | /* Name.Function */
320 |
321 | .highlight .nl {
322 | color: #f8f8f2
323 | }
324 |
325 | /* Name.Label */
326 |
327 | .highlight .nn {
328 | color: #f8f8f2
329 | }
330 |
331 | /* Name.Namespace */
332 |
333 | .highlight .nx {
334 | color: #a6e22e
335 | }
336 |
337 | /* Name.Other */
338 |
339 | .highlight .py {
340 | color: #f8f8f2
341 | }
342 |
343 | /* Name.Property */
344 |
345 | .highlight .nt {
346 | color: #f92672
347 | }
348 |
349 | /* Name.Tag */
350 |
351 | .highlight .nv {
352 | color: #f8f8f2
353 | }
354 |
355 | /* Name.Variable */
356 |
357 | .highlight .ow {
358 | color: #f92672
359 | }
360 |
361 | /* BinaryOperator.Word */
362 |
363 | .highlight .w {
364 | color: #f8f8f2
365 | }
366 |
367 | /* Text.Whitespace */
368 |
369 | .highlight .mf {
370 | color: #ae81ff
371 | }
372 |
373 | /* Literal.Number.Float */
374 |
375 | .highlight .mh {
376 | color: #ae81ff
377 | }
378 |
379 | /* Literal.Number.Hex */
380 |
381 | .highlight .mi {
382 | color: #ae81ff
383 | }
384 |
385 | /* Literal.Number.Integer */
386 |
387 | .highlight .mo {
388 | color: #ae81ff
389 | }
390 |
391 | /* Literal.Number.Oct */
392 |
393 | .highlight .sb {
394 | color: #e6db74
395 | }
396 |
397 | /* Literal.String.Backtick */
398 |
399 | .highlight .sc {
400 | color: #e6db74
401 | }
402 |
403 | /* Literal.String.Char */
404 |
405 | .highlight .sd {
406 | color: #e6db74
407 | }
408 |
409 | /* Literal.String.Doc */
410 |
411 | .highlight .s2 {
412 | color: #e6db74
413 | }
414 |
415 | /* Literal.String.Double */
416 |
417 | .highlight .se {
418 | color: #ae81ff
419 | }
420 |
421 | /* Literal.String.Escape */
422 |
423 | .highlight .sh {
424 | color: #e6db74
425 | }
426 |
427 | /* Literal.String.Heredoc */
428 |
429 | .highlight .si {
430 | color: #e6db74
431 | }
432 |
433 | /* Literal.String.Interpol */
434 |
435 | .highlight .sx {
436 | color: #e6db74
437 | }
438 |
439 | /* Literal.String.Other */
440 |
441 | .highlight .sr {
442 | color: #e6db74
443 | }
444 |
445 | /* Literal.String.Regex */
446 |
447 | .highlight .s1 {
448 | color: #e6db74
449 | }
450 |
451 | /* Literal.String.Single */
452 |
453 | .highlight .ss {
454 | color: #e6db74
455 | }
456 |
457 | /* Literal.String.Symbol */
458 |
459 | .highlight .bp {
460 | color: #f8f8f2
461 | }
462 |
463 | /* Name.Builtin.Pseudo */
464 |
465 | .highlight .vc {
466 | color: #f8f8f2
467 | }
468 |
469 | /* Name.Variable.Class */
470 |
471 | .highlight .vg {
472 | color: #f8f8f2
473 | }
474 |
475 | /* Name.Variable.Global */
476 |
477 | .highlight .vi {
478 | color: #f8f8f2
479 | }
480 |
481 | /* Name.Variable.Instance */
482 |
483 | .highlight .il {
484 | color: #ae81ff
485 | }
486 |
487 | /* Literal.Number.Integer.Long */
488 |
489 | .highlight .gh {}
490 |
491 | /* Generic Heading & Diff Header */
492 |
493 | .highlight .gu {
494 | color: #75715e;
495 | }
496 |
497 | /* Generic.Subheading & Diff Unified/Comment? */
498 |
499 | .highlight .gd {
500 | color: #f92672;
501 | }
502 |
503 | /* Generic.Deleted & Diff Deleted */
504 |
505 | .highlight .gi {
506 | color: #a6e22e;
507 | }
508 |
509 | /* Generic.Inserted & Diff Inserted */
510 |
511 | code.highlighter-rouge {
512 | border: 1px solid rgba(255, 255, 255, .1);
513 | padding: 0 .3em;
514 | border-radius: 3px;
515 | font-size: 1.1em;
516 | }
517 |
518 | pre code {
519 | overflow-x: auto;
520 | display: block;
521 | }
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easafe/purescript-droplet/1b5d86c10b765b615759f2ef021e730735038507/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Getting started
4 | ---
5 |
6 | ## Composable, type-safe eDSL and query mapper for PureScript targeting PostgreSQL
7 |
8 | Droplet is a bit different from other PureScript (or Haskell) SQL libraries. There is no monads, functions to yield columns, or a higher level API abstracting away from the generated SQL query. Instead, the eDSL is made out exclusively of combinators (nearly) identical to their SQL statement counterparts. Likewise, the output of a query is automatically inferred from its projection with almost no need for type annotations or boilerplate mapping type class instances.
9 |
10 | This guide first covers the syntax used by the eDSL, how to run queries using a query mapper, and finally migrations. The eDSL aims to make the keywords you already know (SELECT, WHERE, ORDER BY, JOIN, etc) composable. Likewise, the query mapper is independent from the eDSL -- it can be also used for unsafe queries too. Migrations are accomplished by API or CLI.
11 |
12 | Installation:
13 |
14 | ```
15 | npm i big-integer pg @easafe/pebble
16 |
17 | spago install droplet
18 | ```
19 |
20 | If you'd like to try the code examples, the following schema will be used throughout
21 |
22 | ```sql
23 | create table users (
24 | id integer generated always as identity primary key,
25 | name text not null,
26 | birthday date null
27 | );
28 |
29 | create table messages (
30 | id integer generated always as identity primary key,
31 | sender integer not null,
32 | recipient integer not null,
33 | date timestamptz default now(),
34 |
35 | constraint sender_user foreign key (sender) references users(id),
36 | constraint recipient_user foreign key (recipient) references users(id)
37 | );
38 | ```
39 |
40 |
41 | Next: eDSL
42 |
--------------------------------------------------------------------------------
/docs/mapper.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Query mapper
4 | ---
5 |
6 | ## Querying the database
7 |
8 | Essentially, the query mapper runs a given query and converts its output into records. The first step is to create a connection pool
9 |
10 | ```haskell
11 | newPool :: Configuration -> Effect Pool
12 | ```
13 |
14 | and use one of either
15 |
16 | ```haskell
17 | withConnection :: forall a. Pool -> (Either PgError Connection -> Aff a) -> Aff a
18 |
19 | withTransaction :: forall a. Pool -> (Connection -> Aff a) -> Aff (Either PgError a)
20 | ```
21 |
22 | to obtain a connection. The output of a query always matches the `projection` of a SELECT or RETURNING statement; otherwise, queries with no output (e.g., DELETE or UPDATE) yield `Unit`. The primary querying functions to access the database are
23 |
24 | ```haskell
25 | query ::
26 | forall q projection pro.
27 | ToQuery q projection =>
28 | RowToList projection pro =>
29 | FromResult pro (Record projection) =>
30 | Connection ->
31 | q ->
32 | Aff (Either PgError (Array (Record projection)))
33 |
34 | execute :: forall q. ToQuery q () => Connection -> q -> Aff (Maybe PgError)
35 |
36 | single ::
37 | forall q projection pro.
38 | ToQuery q projection =>
39 | RowToList projection pro =>
40 | FromResult pro (Record projection) =>
41 | Connection ->
42 | q ->
43 | Aff (Either PgError (Maybe (Record projection)))
44 | ```
45 |
46 | `query` fetches an arbitrary number of results. `execute` is intended for queries without an output. `single` is for queries with exactly zero or one results.
47 |
48 | Because queries can be freely composed, a few checks can only be performed when using one of the querying functions above. This includes checking for out of scope column access, ill formed aggregations, invalid top levels, etc.
49 |
50 | For the mapping to work, all columns names in a projection must be unique -- same reason as why literals and functions must be aliased. Since names are always quoted by the query mapper, it is safe to use any casing in columns or aliases. This also means table aliases are not stripped. For example, running the query `SELECT u.id FROM users AS u` on Postgres renders columns as "id"; the Droplet equivalent `select (u ... id) # from (users # as u)` results in the record `{ "u.id" :: Int }`.
51 |
52 | ## Unsafe queries
53 |
54 | For each querying function, there is an unsafe counterpart that accepts raw SQL strings
55 |
56 | ```haskell
57 | unsafeQuery ::
58 | forall projection pro parameters pra.
59 | RowToList projection pro =>
60 | RowToList parameters pra =>
61 | ToParameters parameters pra =>
62 | FromResult pro (Record projection) =>
63 | Connection ->
64 | Maybe Plan ->
65 | String ->
66 | Record parameters ->
67 | Aff (Either PgError (Array (Record projection)))
68 |
69 | unsafeExecute ::
70 | forall parameters pra.
71 | RowToList parameters pra =>
72 | ToParameters parameters pra =>
73 | Connection ->
74 | Maybe Plan ->
75 | String ->
76 | Record parameters ->
77 | Aff (Maybe PgError)
78 |
79 | unsafeSingle ::
80 | forall parameters pra projection pro.
81 | RowToList parameters pra =>
82 | ToParameters parameters pra =>
83 | RowToList projection pro =>
84 | FromResult pro (Record projection) =>
85 | Connection ->
86 | Maybe Plan ->
87 | String ->
88 | Record parameters ->
89 | Aff (Either PgError (Maybe (Record projection)))
90 | ```
91 |
92 | A type annotation will likely be required to determine the output records. The `Maybe Plan` parameter is for prepared statements. To add parameters to your query, use the notation `@name` and include a matching key in the `Record parameters` argument. For example
93 |
94 | ```haskell
95 | selectUnsafe :: Aff (Either PgError (Array {id :: Int}))
96 | selectUnsafe = withConnection pool $ \c -> do
97 | ...
98 | unsafeQuery connection Nothing "SELECT name FROM users WHERE id = @id" { id : 34 }
99 |
100 | selectUnsafe :: Aff (Maybe PgError)
101 | selectUnsafe = withConnection pool $ \c -> do
102 | ...
103 | unsafeExecute connection Nothing "INSERT INTO users (name) VALUES (@name)" { name : "mary" }
104 | ```
105 |
106 | ## Type mapping
107 |
108 | Most common types (integers, strings, dates, etc.) work out of the box. In case the default behavior isn't desirable, or you need to map columns to custom types, the type classes `FromValue` and `ToValue` can perform conversions. `FromValue` translates `Foreign` SQL values to PureScript; `ToValue` parses PureScript values into `Foreign`. All parameters must also have `ToValue` instances.
109 |
110 | Previous: eDSL
111 | Next: Migrations
112 |
--------------------------------------------------------------------------------
/docs/migrations.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Migrations
4 | ---
5 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "purescript-droplet",
3 | "lockfileVersion": 2,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "name": "purescript-droplet",
8 | "dependencies": {
9 | "big-integer": "^1.6.48",
10 | "pg": "^8.6.0"
11 | }
12 | },
13 | "node_modules/big-integer": {
14 | "version": "1.6.48",
15 | "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
16 | "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==",
17 | "engines": {
18 | "node": ">=0.6"
19 | }
20 | },
21 | "node_modules/buffer-writer": {
22 | "version": "2.0.0",
23 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
24 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
25 | "engines": {
26 | "node": ">=4"
27 | }
28 | },
29 | "node_modules/inherits": {
30 | "version": "2.0.4",
31 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
32 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
33 | },
34 | "node_modules/packet-reader": {
35 | "version": "1.0.0",
36 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
37 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
38 | },
39 | "node_modules/pg": {
40 | "version": "8.6.0",
41 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.6.0.tgz",
42 | "integrity": "sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ==",
43 | "dependencies": {
44 | "buffer-writer": "2.0.0",
45 | "packet-reader": "1.0.0",
46 | "pg-connection-string": "^2.5.0",
47 | "pg-pool": "^3.3.0",
48 | "pg-protocol": "^1.5.0",
49 | "pg-types": "^2.1.0",
50 | "pgpass": "1.x"
51 | },
52 | "engines": {
53 | "node": ">= 8.0.0"
54 | },
55 | "peerDependencies": {
56 | "pg-native": ">=2.0.0"
57 | },
58 | "peerDependenciesMeta": {
59 | "pg-native": {
60 | "optional": true
61 | }
62 | }
63 | },
64 | "node_modules/pg-connection-string": {
65 | "version": "2.5.0",
66 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
67 | "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
68 | },
69 | "node_modules/pg-int8": {
70 | "version": "1.0.1",
71 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
72 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
73 | "engines": {
74 | "node": ">=4.0.0"
75 | }
76 | },
77 | "node_modules/pg-pool": {
78 | "version": "3.3.0",
79 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.3.0.tgz",
80 | "integrity": "sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg==",
81 | "peerDependencies": {
82 | "pg": ">=8.0"
83 | }
84 | },
85 | "node_modules/pg-protocol": {
86 | "version": "1.5.0",
87 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
88 | "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
89 | },
90 | "node_modules/pg-types": {
91 | "version": "2.2.0",
92 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
93 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
94 | "dependencies": {
95 | "pg-int8": "1.0.1",
96 | "postgres-array": "~2.0.0",
97 | "postgres-bytea": "~1.0.0",
98 | "postgres-date": "~1.0.4",
99 | "postgres-interval": "^1.1.0"
100 | },
101 | "engines": {
102 | "node": ">=4"
103 | }
104 | },
105 | "node_modules/pgpass": {
106 | "version": "1.0.4",
107 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz",
108 | "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==",
109 | "dependencies": {
110 | "split2": "^3.1.1"
111 | }
112 | },
113 | "node_modules/postgres-array": {
114 | "version": "2.0.0",
115 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
116 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
117 | "engines": {
118 | "node": ">=4"
119 | }
120 | },
121 | "node_modules/postgres-bytea": {
122 | "version": "1.0.0",
123 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
124 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=",
125 | "engines": {
126 | "node": ">=0.10.0"
127 | }
128 | },
129 | "node_modules/postgres-date": {
130 | "version": "1.0.7",
131 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
132 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
133 | "engines": {
134 | "node": ">=0.10.0"
135 | }
136 | },
137 | "node_modules/postgres-interval": {
138 | "version": "1.2.0",
139 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
140 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
141 | "dependencies": {
142 | "xtend": "^4.0.0"
143 | },
144 | "engines": {
145 | "node": ">=0.10.0"
146 | }
147 | },
148 | "node_modules/safe-buffer": {
149 | "version": "5.1.2",
150 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
151 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
152 | },
153 | "node_modules/split2": {
154 | "version": "3.2.2",
155 | "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
156 | "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
157 | "dependencies": {
158 | "readable-stream": "^3.0.0"
159 | }
160 | },
161 | "node_modules/split2/node_modules/readable-stream": {
162 | "version": "3.6.0",
163 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
164 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
165 | "dependencies": {
166 | "inherits": "^2.0.3",
167 | "string_decoder": "^1.1.1",
168 | "util-deprecate": "^1.0.1"
169 | },
170 | "engines": {
171 | "node": ">= 6"
172 | }
173 | },
174 | "node_modules/string_decoder": {
175 | "version": "1.1.1",
176 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
177 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
178 | "dependencies": {
179 | "safe-buffer": "~5.1.0"
180 | }
181 | },
182 | "node_modules/util-deprecate": {
183 | "version": "1.0.2",
184 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
185 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
186 | },
187 | "node_modules/xtend": {
188 | "version": "4.0.2",
189 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
190 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
191 | "engines": {
192 | "node": ">=0.4"
193 | }
194 | }
195 | },
196 | "dependencies": {
197 | "big-integer": {
198 | "version": "1.6.48",
199 | "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
200 | "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
201 | },
202 | "buffer-writer": {
203 | "version": "2.0.0",
204 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
205 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
206 | },
207 | "inherits": {
208 | "version": "2.0.4",
209 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
210 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
211 | },
212 | "packet-reader": {
213 | "version": "1.0.0",
214 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
215 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
216 | },
217 | "pg": {
218 | "version": "8.6.0",
219 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.6.0.tgz",
220 | "integrity": "sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ==",
221 | "requires": {
222 | "buffer-writer": "2.0.0",
223 | "packet-reader": "1.0.0",
224 | "pg-connection-string": "^2.5.0",
225 | "pg-pool": "^3.3.0",
226 | "pg-protocol": "^1.5.0",
227 | "pg-types": "^2.1.0",
228 | "pgpass": "1.x"
229 | }
230 | },
231 | "pg-connection-string": {
232 | "version": "2.5.0",
233 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
234 | "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
235 | },
236 | "pg-int8": {
237 | "version": "1.0.1",
238 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
239 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
240 | },
241 | "pg-pool": {
242 | "version": "3.3.0",
243 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.3.0.tgz",
244 | "integrity": "sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg==",
245 | "requires": {}
246 | },
247 | "pg-protocol": {
248 | "version": "1.5.0",
249 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
250 | "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
251 | },
252 | "pg-types": {
253 | "version": "2.2.0",
254 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
255 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
256 | "requires": {
257 | "pg-int8": "1.0.1",
258 | "postgres-array": "~2.0.0",
259 | "postgres-bytea": "~1.0.0",
260 | "postgres-date": "~1.0.4",
261 | "postgres-interval": "^1.1.0"
262 | }
263 | },
264 | "pgpass": {
265 | "version": "1.0.4",
266 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz",
267 | "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==",
268 | "requires": {
269 | "split2": "^3.1.1"
270 | }
271 | },
272 | "postgres-array": {
273 | "version": "2.0.0",
274 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
275 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
276 | },
277 | "postgres-bytea": {
278 | "version": "1.0.0",
279 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
280 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU="
281 | },
282 | "postgres-date": {
283 | "version": "1.0.7",
284 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
285 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="
286 | },
287 | "postgres-interval": {
288 | "version": "1.2.0",
289 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
290 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
291 | "requires": {
292 | "xtend": "^4.0.0"
293 | }
294 | },
295 | "safe-buffer": {
296 | "version": "5.1.2",
297 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
298 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
299 | },
300 | "split2": {
301 | "version": "3.2.2",
302 | "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
303 | "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
304 | "requires": {
305 | "readable-stream": "^3.0.0"
306 | },
307 | "dependencies": {
308 | "readable-stream": {
309 | "version": "3.6.0",
310 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
311 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
312 | "requires": {
313 | "inherits": "^2.0.3",
314 | "string_decoder": "^1.1.1",
315 | "util-deprecate": "^1.0.1"
316 | }
317 | }
318 | }
319 | },
320 | "string_decoder": {
321 | "version": "1.1.1",
322 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
323 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
324 | "requires": {
325 | "safe-buffer": "~5.1.0"
326 | }
327 | },
328 | "util-deprecate": {
329 | "version": "1.0.2",
330 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
331 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
332 | },
333 | "xtend": {
334 | "version": "4.0.2",
335 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
336 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "purescript-droplet",
3 | "dependencies": {
4 | "big-integer": "^1.6.48",
5 | "pg": "^8.7.1"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages.dhall:
--------------------------------------------------------------------------------
1 | {-
2 | Welcome to your new Dhall package-set!
3 |
4 | Below are instructions for how to edit this file for most use
5 | cases, so that you don't need to know Dhall to use it.
6 |
7 | ## Warning: Don't Move This Top-Level Comment!
8 |
9 | Due to how `dhall format` currently works, this comment's
10 | instructions cannot appear near corresponding sections below
11 | because `dhall format` will delete the comment. However,
12 | it will not delete a top-level comment like this one.
13 |
14 | ## Use Cases
15 |
16 | Most will want to do one or both of these options:
17 | 1. Override/Patch a package's dependency
18 | 2. Add a package not already in the default package set
19 |
20 | This file will continue to work whether you use one or both options.
21 | Instructions for each option are explained below.
22 |
23 | ### Overriding/Patching a package
24 |
25 | Purpose:
26 | - Change a package's dependency to a newer/older release than the
27 | default package set's release
28 | - Use your own modified version of some dependency that may
29 | include new API, changed API, removed API by
30 | using your custom git repo of the library rather than
31 | the package set's repo
32 |
33 | Syntax:
34 | where `entityName` is one of the following:
35 | - dependencies
36 | - repo
37 | - version
38 | -------------------------------
39 | let upstream = --
40 | in upstream
41 | with packageName.entityName = "new value"
42 | -------------------------------
43 |
44 | Example:
45 | -------------------------------
46 | let upstream = --
47 | in upstream
48 | with halogen.version = "master"
49 | with halogen.repo = "https://example.com/path/to/git/repo.git"
50 |
51 | with halogen-vdom.version = "v4.0.0"
52 | -------------------------------
53 |
54 | ### Additions
55 |
56 | Purpose:
57 | - Add packages that aren't already included in the default package set
58 |
59 | Syntax:
60 | where `` is:
61 | - a tag (i.e. "v4.0.0")
62 | - a branch (i.e. "master")
63 | - commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977")
64 | -------------------------------
65 | let upstream = --
66 | in upstream
67 | with new-package-"name" =
68 | { dependencies =
69 | [ "dependency1"
70 | , "dependency2"
71 | ]
72 | , repo =
73 | "https://example.com/path/to/git/repo.git"
74 | , version =
75 | ""
76 | }
77 | -------------------------------
78 |
79 | Example:
80 | -------------------------------
81 | let upstream = --
82 | in upstream
83 | with benchotron =
84 | { dependencies =
85 | [ "arrays"
86 | , "exists"
87 | , "profunctor"
88 | , "strings"
89 | , "quickcheck"
90 | , "lcg"
91 | , "transformers"
92 | , "foldable-traversable"
93 | , "exceptions"
94 | , "node-fs"
95 | , "node-buffer"
96 | , "node-readline"
97 | , "datetime"
98 | , "now"
99 | ]
100 | , repo =
101 | "https://github.com/hdgarrood/purescript-benchotron.git"
102 | , version =
103 | "v7.0.0"
104 | }
105 | -------------------------------
106 | -}
107 | let upstream =
108 | https://github.com/purescript/package-sets/releases/download/psc-0.15.0-20220504/packages.dhall
109 | sha256:fd37736ecaa24491c907af6a6422156417f21fbf25763de19f65bd641e8340d3
110 |
111 | in upstream
112 |
--------------------------------------------------------------------------------
/spago.dhall:
--------------------------------------------------------------------------------
1 | {-
2 | Welcome to a Spago project!
3 | You can edit this file as you like.
4 | -}
5 | { name = "droplet"
6 | , license = "MIT"
7 | , repository = "https://github.com/easafe/purescript-droplet"
8 | , dependencies =
9 | [ "aff"
10 | , "arrays"
11 | , "bifunctors"
12 | , "bigints"
13 | , "datetime"
14 | , "debug"
15 | , "effect"
16 | , "either"
17 | , "enums"
18 | , "exceptions"
19 | , "foldable-traversable"
20 | , "foreign"
21 | , "foreign-object"
22 | , "integers"
23 | , "maybe"
24 | , "newtype"
25 | , "nonempty"
26 | , "nullable"
27 | , "ordered-collections"
28 | , "partial"
29 | , "prelude"
30 | , "profunctor"
31 | , "record"
32 | , "spec"
33 | , "strings"
34 | , "transformers"
35 | , "tuples"
36 | , "type-equality"
37 | , "typelevel-prelude"
38 | , "unsafe-coerce"
39 | ]
40 | , packages = ./packages.dhall
41 | , sources = [ "src/**/*.purs", "test/**/*.purs" ]
42 | }
43 |
--------------------------------------------------------------------------------
/src/Droplet/Driver.purs:
--------------------------------------------------------------------------------
1 | -- | Functions for database access
2 | module Droplet.Driver (module Exported) where
3 |
4 | import Droplet.Language.Internal.Translate (Query) as Exported
5 |
6 | import Droplet.Driver.Internal.Query (class FromResult, Connection, PGErrorDetail, PgError(..), connect, execute, query, single, toResult, withConnection, withTransaction) as Exported
7 |
8 | import Droplet.Driver.Internal.Pool (Configuration, Database, Pool, defaultConfiguration, newPool) as Exported
--------------------------------------------------------------------------------
/src/Droplet/Driver/Internal/Migration.purs:
--------------------------------------------------------------------------------
1 | module Droplet.Driver.Internal.Migration where
2 |
3 | import Prelude
4 |
5 | import Data.Either (Either(..))
6 | import Data.Maybe (Maybe(..))
7 | import Data.Set (Set)
8 | import Data.Set as DS
9 | import Droplet.Driver.Internal.Query (Connection(..))
10 | import Droplet.Driver.Internal.Query as DDIQ
11 | import Droplet.Language (class ToParameters)
12 | import Effect.Aff (Aff)
13 | import Effect.Aff as EA
14 | import Prim.RowList (class RowToList)
15 |
16 | markAsRun ∷ forall r. Connection → { identifier :: String | r } → Aff Unit
17 | markAsRun connection migration =
18 | execute connection "INSERT INTO __droplet_migrations__ (identifier) VALUES (@identifier)" {identifier: migration.identifier}
19 |
20 | createMigrationTable ∷ Connection → Aff Unit
21 | createMigrationTable connection = execute connection "CREATE TABLE IF NOT EXISTS __droplet_migrations__ (identifier TEXT NOT NULL PRIMARY KEY, run_at TIMESTAMPTZ NOT NULL DEFAULT NOW())" {}
22 |
23 | execute :: forall parameters list. RowToList parameters list => ToParameters parameters list => Connection -> String -> Record parameters -> Aff Unit
24 | execute connection sql parameters = do
25 | output <- DDIQ.unsafeExecute connection Nothing sql parameters
26 | case output of
27 | Just err -> throw err
28 | Nothing -> pure unit
29 |
30 | fetchAlreadyRun ∷ Connection → Aff (Set String)
31 | fetchAlreadyRun connection = do
32 | output ← DDIQ.unsafeQuery connection Nothing "SELECT identifier FROM __droplet_migrations__" {}
33 | case output of
34 | Right (rows :: Array { identifier :: String }) -> pure <<< DS.fromFoldable $ map _.identifier rows
35 | Left err → throw err
36 |
37 | throw err = EA.throwError <<< EA.error $ show err
38 |
--------------------------------------------------------------------------------
/src/Droplet/Driver/Internal/Pool.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import pg from 'pg';
4 |
5 | export function newPool_(config) {
6 | return function() {
7 | return new pg.Pool(config);
8 | };
9 | }
--------------------------------------------------------------------------------
/src/Droplet/Driver/Internal/Pool.purs:
--------------------------------------------------------------------------------
1 | module Droplet.Driver.Internal.Pool (Configuration, Database, Pool, defaultConfiguration, newPool) where
2 |
3 | import Data.Maybe (Maybe(..))
4 | import Data.Nullable (Nullable, toNullable)
5 | import Effect (Effect)
6 |
7 | -- | PostgreSQL connection pool.
8 | foreign import data Pool ∷ Type
9 |
10 | type Database = String
11 |
12 | -- | Configuration which we actually pass to FFI.
13 | type Configuration' =
14 | { user ∷ Nullable String
15 | , password ∷ Nullable String
16 | , host ∷ Nullable String
17 | , port ∷ Nullable Int
18 | , database ∷ String
19 | , max ∷ Nullable Int
20 | , idleTimeoutMillis ∷ Nullable Int
21 | }
22 |
23 | -- | PostgreSQL connection pool configuration.
24 | type Configuration =
25 | { database ∷ Database
26 | , host ∷ Maybe String
27 | , idleTimeoutMillis ∷ Maybe Int
28 | , max ∷ Maybe Int
29 | , password ∷ Maybe String
30 | , port ∷ Maybe Int
31 | , user ∷ Maybe String
32 | }
33 |
34 | foreign import newPool_ ∷ Configuration' → Effect Pool
35 |
36 | defaultConfiguration ∷ Database → Configuration
37 | defaultConfiguration database =
38 | { database
39 | , host: Nothing
40 | , idleTimeoutMillis: Nothing
41 | , max: Nothing
42 | , password: Nothing
43 | , port: Nothing
44 | , user: Nothing
45 | }
46 |
47 | -- | Create a new connection pool.
48 | newPool ∷ Configuration → Effect Pool
49 | newPool cfg = newPool_
50 | { user: toNullable cfg.user
51 | , password: toNullable cfg.password
52 | , host: toNullable cfg.host
53 | , port: toNullable cfg.port
54 | , database: cfg.database
55 | , max: toNullable cfg.max
56 | , idleTimeoutMillis: toNullable cfg.idleTimeoutMillis
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/Droplet/Driver/Internal/Query.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import pg from 'pg';
4 |
5 | function id(x) { return x; }
6 |
7 | pg.types.setTypeParser(1082 /* DATE_OID */, id);
8 | pg.types.setTypeParser(1114 /* TIMESTAMP_OID */, id);
9 | pg.types.setTypeParser(1184 /* TIMESTAMPTZ_OID */, id);
10 |
11 | export function connect_(config) {
12 | return function (pool) {
13 | return function (onError, onSuccess) {
14 | var p = pool.connect().then(function (client) {
15 | onSuccess(config.right({
16 | client: client,
17 | done: function () {
18 | return client.release();
19 | }
20 | }));
21 | }).catch(function (err) {
22 | var pgError = config.nullableLeft(err);
23 | if (pgError) {
24 | onSuccess(pgError);
25 | } else {
26 | onError(err);
27 | }
28 | });
29 |
30 | return function (cancelError, cancelerError, cancelerSuccess) {
31 | p.cancel();
32 | cancelerSuccess();
33 | };
34 | };
35 | };
36 | }
37 |
38 | export function rawQuery_(config) {
39 | return function (dbHandle) {
40 | return function (rq) {
41 | return function (onError, onSuccess) {
42 | var q = dbHandle.query({
43 | name: rq.name,
44 | text: rq.text,
45 | values: rq.values
46 | }).then(function (result) {
47 | onSuccess(config.right(result.rows));
48 | }).catch(function (err) {
49 | var pgError = config.nullableLeft(err);
50 | if (pgError) {
51 | onSuccess(pgError);
52 | } else {
53 | onError(err);
54 | }
55 | });
56 |
57 | return function (_, __, cancelerSuccess) {
58 | q.cancel();
59 | cancelerSuccess();
60 | };
61 | };
62 | };
63 | };
64 | }
65 |
66 | export function sqlState_(error) {
67 | return error.code || null;
68 | }
69 |
70 | export function errorDetail_(error) {
71 | return {
72 | error: error,
73 | severity: error.severity || '',
74 | code: error.code || '',
75 | message: error.message || '',
76 | detail: error.detail || '',
77 | hint: error.hint || '',
78 | position: error.position || '',
79 | internalPosition: error.internalPosition || '',
80 | internalQuery: error.internalQuery || '',
81 | where_: error.where || '',
82 | schema: error.schema || '',
83 | table: error.table || '',
84 | column: error.column || '',
85 | dataType: error.dataType || '',
86 | constraint: error.constraint || '',
87 | file: error.file || '',
88 | line: error.line || '',
89 | routine: error.routine || ''
90 | };
91 | }
92 |
--------------------------------------------------------------------------------
/src/Droplet/Driver/Internal/Query.purs:
--------------------------------------------------------------------------------
1 | module Droplet.Driver.Internal.Query (class FromResult, Connection(..), Client, PGErrorDetail, PgError(..), connect, execute, query, single, toResult, unsafeExecute, unsafeQuery, unsafeSingle, withConnection, withTransaction) where
2 |
3 | import Prelude
4 |
5 | import Data.Bifunctor as DB
6 | import Data.Either (Either(..))
7 | import Data.Generic.Rep (class Generic)
8 | import Data.Maybe (Maybe(..), maybe)
9 | import Data.Newtype (class Newtype)
10 | import Data.Nullable (Nullable, null, toMaybe, toNullable)
11 | import Data.Nullable as DN
12 | import Data.Profunctor (lcmap)
13 | import Data.Reflectable (class Reflectable)
14 | import Data.Reflectable as DR
15 | import Data.Show.Generic as DSG
16 | import Data.String (Pattern(..))
17 | import Data.String as DST
18 | import Data.Symbol (class IsSymbol)
19 | import Data.Traversable as DT
20 | import Droplet.Driver.Internal.Pool (Pool)
21 | import Droplet.Language.Internal.Definition (class FromValue, class ToParameters)
22 | import Droplet.Language.Internal.Definition as DIED
23 | import Droplet.Language.Internal.Syntax (Plan(..))
24 | import Droplet.Language.Internal.Translate (class ToQuery, Query(..))
25 | import Droplet.Language.Internal.Translate as DIMQ
26 | import Effect (Effect)
27 | import Effect.Aff (Aff, bracket)
28 | import Effect.Aff as EA
29 | import Effect.Aff.Compat (EffectFnAff)
30 | import Effect.Aff.Compat as EAC
31 | import Effect.Class (liftEffect)
32 | import Effect.Exception (Error)
33 | import Foreign (Foreign)
34 | import Foreign.Object (Object)
35 | import Foreign.Object as FO
36 | import Prim.Row (class Cons, class Lacks)
37 | import Prim.RowList (class RowToList, RowList)
38 | import Prim.RowList as RL
39 | import Record (delete) as Record
40 | import Record as R
41 | import Type.Proxy (Proxy(..))
42 | import Unsafe.Coerce as UC
43 |
44 | type ConnectResult =
45 | { client ∷ Client
46 | , done ∷ Effect Unit
47 | }
48 |
49 | data PgError
50 | = ClientError Error String
51 | | ConversionError String
52 | | InternalError PGErrorDetail
53 | | OperationalError PGErrorDetail
54 | | ProgrammingError PGErrorDetail
55 | | IntegrityError PGErrorDetail
56 | | DataError PGErrorDetail
57 | | NotSupportedError PGErrorDetail
58 | | QueryCanceledError PGErrorDetail
59 | | TransactionRollbackError PGErrorDetail
60 | | TooManyRows
61 |
62 | type PGErrorDetail =
63 | { severity ∷ String
64 | , code ∷ String
65 | , message ∷ String
66 | , detail ∷ String
67 | , error ∷ Error
68 | , hint ∷ String
69 | , position ∷ String
70 | , internalPosition ∷ String
71 | , internalQuery ∷ String
72 | , where_ ∷ String
73 | , schema ∷ String
74 | , table ∷ String
75 | , column ∷ String
76 | , dataType ∷ String
77 | , constraint ∷ String
78 | , file ∷ String
79 | , line ∷ String
80 | , routine ∷ String
81 | }
82 |
83 | newtype Connection = Connection (Either Pool Client)
84 |
85 | -- | APIs of the `Pool.query` and `Client.query` are the same.
86 | -- | We can dse this polymorphism to simplify ffi.
87 | foreign import data UntaggedConnection ∷ Type
88 |
89 | -- | PostgreSQL connection.
90 | foreign import data Client ∷ Type
91 |
92 | type RawQuery =
93 | { name ∷ Nullable String
94 | , text ∷ String
95 | , values ∷ Array Foreign
96 | }
97 |
98 | -- | Those instances are required for testing.
99 | instance eqPGError ∷ Eq PgError where
100 | eq = case _, _ of
101 | ClientError _ s1, ClientError _ s2 → s1 == s2
102 | ConversionError s1, ConversionError s2 → s1 == s2
103 | InternalError err1, InternalError err2 → eqErr err1 err2
104 | OperationalError err1, OperationalError err2 → eqErr err1 err2
105 | ProgrammingError err1, ProgrammingError err2 → eqErr err1 err2
106 | IntegrityError err1, IntegrityError err2 → eqErr err1 err2
107 | DataError err1, DataError err2 → eqErr err1 err2
108 | NotSupportedError err1, NotSupportedError err2 → eqErr err1 err2
109 | QueryCanceledError err1, QueryCanceledError err2 → eqErr err1 err2
110 | TransactionRollbackError err1, TransactionRollbackError err2 → eqErr err1 err2
111 | _, _ → false
112 | where
113 | eqErr err1 err2 =
114 | let _error = Proxy ∷ Proxy "error" in eq (Record.delete _error err1) (Record.delete _error err2)
115 |
116 | derive instance Generic PgError _
117 | derive instance Newtype Connection _
118 | instance Show PgError where
119 | show = DSG.genericShow
120 |
121 | class FromResult (projection ∷ RowList Type) result | projection → result where
122 | toResult ∷ Proxy projection → Object Foreign → Either String result
123 |
124 | instance nilFromResult ∷ FromResult RL.Nil (Record ()) where
125 | toResult _ _ = Right {}
126 |
127 | instance consFromResult ∷
128 | ( FromValue t
129 | , FromResult rest (Record restProjection)
130 | , IsSymbol name
131 | , Reflectable name String
132 | , Lacks name restProjection
133 | , Cons name t restProjection projection
134 | ) ⇒
135 | FromResult (RL.Cons name t rest) (Record projection) where
136 | toResult _ raw = case FO.lookup field raw of
137 | Nothing → Left $ "Could not find column matching field: " <> field
138 | Just value → case DIED.fromValue value of
139 | Left error → Left $ "While parsing field " <> field <> ": " <> error
140 | Right converted → map (R.insert name converted) $ toResult (Proxy ∷ Proxy rest) raw
141 | where
142 | name = Proxy ∷ Proxy name
143 | field = DR.reflectType name
144 |
145 | foreign import connect_ ∷
146 | ∀ a.
147 | { nullableLeft ∷ Error → Nullable (Either PgError ConnectResult)
148 | , right ∷ a → Either PgError ConnectResult
149 | } →
150 | Pool →
151 | EffectFnAff (Either PgError ConnectResult)
152 |
153 | foreign import rawQuery_ ∷
154 | { nullableLeft ∷ Error → Nullable (Either PgError (Array (Object Foreign)))
155 | , right ∷ Array (Object Foreign) → Either PgError (Array (Object Foreign))
156 | } →
157 | UntaggedConnection →
158 | RawQuery →
159 | EffectFnAff (Either PgError (Array (Object Foreign)))
160 |
161 | foreign import sqlState_ ∷ Error → Nullable String
162 | foreign import errorDetail_ ∷ Error → PGErrorDetail
163 |
164 | -------------------------------CONNECTING----------------------------------------------
165 |
166 | -- | Run an action with a connection. The connection is released to the pool
167 | -- | when the action returns.
168 | withClient ∷ ∀ a. Pool → (Either PgError Client → Aff a) → Aff a
169 | withClient p k = bracket (connect p) cleanup run
170 | where
171 | cleanup = case _ of
172 | Left _ → pure unit
173 | Right { done } → liftEffect done
174 |
175 | run = case _ of
176 | Left err → k $ Left err
177 | Right { client } → k $ Right client
178 |
179 | connect ∷ Pool → Aff (Either PgError ConnectResult)
180 | connect = EAC.fromEffectFnAff <<< connect_ rightLeft
181 |
182 | -- | Runs queries with a connection
183 | withConnection ∷ ∀ a. Pool → (Either PgError Connection → Aff a) → Aff a
184 | withConnection p k = withClient p (lcmap (map fromClient) k)
185 |
186 | -- | Runs queries within a transaction
187 | withTransaction ∷ ∀ a. Pool → (Connection → Aff a) → Aff (Either PgError a)
188 | withTransaction pool action = withClient pool case _ of
189 | Right client → withClientTransaction client (action $ fromClient client)
190 | Left err → pure $ Left err
191 |
192 | withClientTransaction ∷ ∀ a. Client → Aff a → Aff (Either PgError a)
193 | withClientTransaction client action =
194 | begin >>= case _ of
195 | Nothing → do
196 | a ← action `EA.catchError` \jsErr → do
197 | void $ rollback
198 | EA.throwError jsErr
199 | commit >>= case _ of
200 | Nothing → pure $ Right a
201 | Just pgError → pure $ Left pgError
202 | Just pgError → pure $ Left pgError
203 | where
204 | connection = fromClient client
205 | begin = unsafeExecute connection Nothing "BEGIN TRANSACTION" {}
206 | commit = unsafeExecute connection Nothing "COMMIT TRANSACTION" {}
207 | rollback = unsafeExecute connection Nothing "ROLLBACK TRANSACTION" {}
208 |
209 | -------------------------------QUERYING----------------------------------
210 |
211 | -- | Returns an array of `projection` records
212 | query ∷
213 | ∀ q projection pro.
214 | ToQuery q projection ⇒
215 | RowToList projection pro ⇒
216 | FromResult pro (Record projection) ⇒
217 | Connection →
218 | q →
219 | Aff (Either PgError (Array (Record projection)))
220 | query connection q = do
221 | raw ← rawResults
222 | case raw of
223 | Left error → pure $ Left error
224 | Right r → pure $ DT.traverse (DB.lmap ConversionError <<< toResult (Proxy ∷ Proxy pro)) r
225 | where
226 | Query plan sql parameters = DIMQ.buildQuery q
227 |
228 | rawResults ∷ Aff (Either PgError (Array (Object Foreign)))
229 | rawResults = EAC.fromEffectFnAff $ rawQuery_ rightLeft (toUntaggedHandler connection)
230 | { name: case plan of
231 | Nothing → null
232 | Just (Plan name) → DN.notNull name
233 | , text: sql
234 | , values: parameters
235 | }
236 |
237 | -- | Returns an array of `projection` records from a SQL string
238 | unsafeQuery ∷
239 | ∀ projection pro parameters pra.
240 | RowToList projection pro ⇒
241 | RowToList parameters pra ⇒
242 | ToParameters parameters pra ⇒
243 | FromResult pro (Record projection) ⇒
244 | Connection →
245 | Maybe Plan →
246 | String →
247 | Record parameters →
248 | Aff (Either PgError (Array (Record projection)))
249 | unsafeQuery connection plan q parameters = query connection $ DIMQ.unsafeBuildQuery plan q parameters
250 |
251 | -- | Runs a query without results
252 | execute ∷ ∀ q. ToQuery q () ⇒ Connection → q → Aff (Maybe PgError)
253 | execute connection q = do
254 | results ← query connection q
255 | pure $ case results of
256 | Left err → Just err
257 | _ → Nothing
258 |
259 | -- | Runs a query without results from a SQL string
260 | unsafeExecute ∷
261 | ∀ parameters pra.
262 | RowToList parameters pra ⇒
263 | ToParameters parameters pra ⇒
264 | Connection →
265 | Maybe Plan →
266 | String →
267 | Record parameters →
268 | Aff (Maybe PgError)
269 | unsafeExecute connection plan q parameters = do
270 | results ← query connection (DIMQ.unsafeBuildQuery plan q parameters ∷ Query ())
271 | pure $ case results of
272 | Left err → Just err
273 | _ → Nothing
274 |
275 | -- | Runs a query that returns zero or one results
276 | single ∷
277 | ∀ q projection pro.
278 | ToQuery q projection ⇒
279 | RowToList projection pro ⇒
280 | FromResult pro (Record projection) ⇒
281 | Connection →
282 | q →
283 | Aff (Either PgError (Maybe (Record projection)))
284 | single connection q = do
285 | results ← query connection q
286 | pure $ case results of
287 | Left err → Left err
288 | Right [] → Right Nothing
289 | Right [ r ] → Right $ Just r
290 | _ → Left TooManyRows
291 |
292 | -- | Runs a query that returns zero or one results from a SQL string
293 | unsafeSingle ∷
294 | ∀ parameters pra projection pro.
295 | RowToList parameters pra ⇒
296 | ToParameters parameters pra ⇒
297 | RowToList projection pro ⇒
298 | FromResult pro (Record projection) ⇒
299 | Connection →
300 | Maybe Plan →
301 | String →
302 | Record parameters →
303 | Aff (Either PgError (Maybe (Record projection)))
304 | unsafeSingle connection plan q parameters = single connection $ DIMQ.unsafeBuildQuery plan q parameters
305 |
306 | toUntaggedHandler ∷ Connection → UntaggedConnection
307 | toUntaggedHandler (Connection c) = case c of
308 | Left pool → UC.unsafeCoerce pool
309 | Right client → UC.unsafeCoerce client
310 |
311 | fromClient ∷ Client → Connection
312 | fromClient client = Connection (Right client)
313 |
314 | rightLeft ∷
315 | ∀ r s t.
316 | { nullableLeft ∷ Error → Nullable (Either PgError r)
317 | , right ∷ t → Either s t
318 | }
319 | rightLeft =
320 | { nullableLeft: toNullable <<< map Left <<< convertError
321 | , right: Right
322 | }
323 |
324 | convertError ∷ Error → Maybe PgError
325 | convertError err = case toMaybe $ sqlState_ err of
326 | Nothing → Nothing
327 | Just sqlState → Just $ convert sqlState $ errorDetail_ err
328 | where
329 | convert ∷ String → PGErrorDetail → PgError
330 | convert s =
331 | if prefix "0A" s then
332 | NotSupportedError
333 | else if prefix "20" s || prefix "21" s then
334 | ProgrammingError
335 | else if prefix "22" s then
336 | DataError
337 | else if prefix "23" s then
338 | IntegrityError
339 | else if prefix "24" s || prefix "25" s then
340 | InternalError
341 | else if prefix "26" s || prefix "27" s || prefix "28" s then
342 | OperationalError
343 | else if prefix "2B" s || prefix "2D" s || prefix "2F" s then
344 | InternalError
345 | else if prefix "34" s then
346 | OperationalError
347 | else if prefix "38" s || prefix "39" s || prefix "3B" s then
348 | InternalError
349 | else if prefix "3D" s || prefix "3F" s then
350 | ProgrammingError
351 | else if prefix "40" s then
352 | TransactionRollbackError
353 | else if prefix "42" s || prefix "44" s then
354 | ProgrammingError
355 | else if s == "57014" then
356 | QueryCanceledError
357 | else if prefix "5" s then
358 | OperationalError
359 | else if prefix "F" s then
360 | InternalError
361 | else if prefix "H" s then
362 | OperationalError
363 | else if prefix "P" s then
364 | InternalError
365 | else if prefix "X" s then
366 | InternalError
367 | else
368 | const $ ClientError err s
369 |
370 | prefix ∷ String → String → Boolean
371 | prefix p = maybe false (_ == 0) <<< DST.indexOf (Pattern p)
372 |
--------------------------------------------------------------------------------
/src/Droplet/Driver/Migration.purs:
--------------------------------------------------------------------------------
1 | module Droplet.Driver.Migration (Migration, migrate) where
2 |
3 | import Prelude
4 |
5 | import Data.Array as DA
6 | import Data.Either (Either(..))
7 | import Data.Foldable as DF
8 | import Data.Maybe (Maybe(..))
9 | import Data.Set (Set)
10 | import Data.Set as DS
11 | import Debug (spy)
12 | import Droplet.Driver.Internal.Migration as DDIM
13 | import Droplet.Driver.Internal.Pool (Pool)
14 | import Droplet.Driver.Internal.Query (Connection)
15 | import Droplet.Driver.Internal.Query as DDIQ
16 | import Droplet.Language (class ToParameters)
17 | import Effect.Aff (Aff)
18 | import Effect.Aff as EA
19 | import Prim.RowList (class RowToList)
20 |
21 | -- | Type for individual migrations
22 | -- |
23 | -- | - `up` performs a migration
24 | -- | - `down` reverts a migration
25 | -- | - `identifier` must be unique
26 | type Migration =
27 | { up ∷ Connection → Aff Unit
28 | , down ∷ Connection → Aff Unit
29 | , identifier ∷ String
30 | }
31 |
32 | -- | Runs migrations
33 | -- |
34 | -- | All migrations run in a single transaction. Migrations that have already been run are skipped
35 | migrate ∷ Pool → Array Migration → Aff Unit
36 | migrate pool migrations = do
37 | output <- DDIQ.withTransaction pool $ \connection -> do
38 | DDIM.createMigrationTable connection
39 | identifiers <- DDIM.fetchAlreadyRun connection
40 | DF.traverse_ (runMigration connection) $ skipAlreadyRun identifiers migrations
41 | case output of
42 | Left err → DDIM.throw err
43 | Right _ → pure unit
44 |
45 | runMigration ∷ Connection → Migration → Aff Unit
46 | runMigration connection migration = do
47 | migration.up connection
48 | DDIM.markAsRun connection migration
49 |
50 | skipAlreadyRun ∷ Set String → Array Migration → Array Migration
51 | skipAlreadyRun identifiers = DA.filter skip
52 | where skip migration = not $ DS.member migration.identifier identifiers
53 |
--------------------------------------------------------------------------------
/src/Droplet/Driver/Unsafe.purs:
--------------------------------------------------------------------------------
1 | -- | Unsafe query functions
2 | module Droplet.Driver.Unsafe (module Exported) where
3 |
4 | import Droplet.Driver.Internal.Query (unsafeExecute, unsafeQuery, unsafeSingle) as Exported
5 |
--------------------------------------------------------------------------------
/src/Droplet/Language.purs:
--------------------------------------------------------------------------------
1 | -- | Query language
2 | module Droplet.Language (spyQuery, module Exported) where
3 |
4 | import Data.Maybe (Maybe)
5 | import Droplet.Language.Internal.Condition
6 | ( and
7 | , equals
8 | , isNull
9 | , isNotNull
10 | , notEquals
11 | , greaterThan
12 | , lesserThan
13 | , greaterEqualsThan
14 | , lesserEqualsThan
15 | , or
16 | , not
17 | , in_
18 | , Op
19 | , Not
20 | , In
21 | , Exists
22 | , (.&&.)
23 | , (.<>.)
24 | , (.=.)
25 | , (.>.)
26 | , (.<.)
27 | , (.>=.)
28 | , (.<=.)
29 | , (.||.)
30 | ) as Exported
31 | import Droplet.Language.Internal.Definition
32 | ( class FromValue
33 | , class ToParameters
34 | , class ToValue
35 | , class ToType
36 | , Identity
37 | , Column(..)
38 | , ForeignKey
39 | , Default(..)
40 | , Composite
41 | , PrimaryKey
42 | , Constraint
43 | , Unique
44 | , E
45 | , Path
46 | , Star(..)
47 | , Table(..)
48 | , fromValue
49 | , star
50 | , toType
51 | , toValue
52 | , (...)
53 | ) as Exported
54 | import Droplet.Language.Internal.Function
55 | ( count
56 | , coalesce
57 | , string_agg
58 | , array_agg
59 | , function
60 | , function'
61 | , Aggregate
62 | , FunctionSignature
63 | , FunctionSignature'
64 | , random
65 | ) as Exported
66 | import Droplet.Language.Internal.Translate (class ToQuery, Query(..))
67 |
68 | import Droplet.Language.Internal.Translate as DLIQ
69 | import Droplet.Language.Internal.Syntax
70 | ( As
71 | , Inner
72 | , Outer
73 | , Delete
74 | , From
75 | , Insert
76 | , Into
77 | , OrderBy
78 | , Plan
79 | , Prepare
80 | , Alter
81 | , Select
82 | , Drop
83 | , Set
84 | , Update
85 | , Join
86 | , Offset
87 | , Returning
88 | , T
89 | , Limit
90 | , Create
91 | , Values
92 | , On
93 | , Distinct
94 | , Where
95 | , DefaultValues
96 | , Union
97 | , as
98 | , delete
99 | , from
100 | , defaultValues
101 | , insert
102 | , drop
103 | , create
104 | , into
105 | , alter
106 | , table
107 | , orderBy
108 | , join
109 | , leftJoin
110 | , groupBy
111 | , offset
112 | , union
113 | , unionAll
114 | , distinct
115 | , exists
116 | , prepare
117 | , select
118 | , set
119 | , asc
120 | , add
121 | , on
122 | , desc
123 | , update
124 | , limit
125 | , values
126 | , wher
127 | , returning
128 | ) as Exported
129 | import Droplet.Language.Internal.Syntax (Plan)
130 | import Foreign (Foreign)
131 |
132 | -- | Debug generated query
133 | spyQuery ∷
134 | ∀ q projection.
135 | ToQuery q projection ⇒
136 | q →
137 | { parameters ∷ Array Foreign
138 | , plan ∷ Maybe Plan
139 | , query ∷ String
140 | }
141 | spyQuery q = case DLIQ.buildQuery q of
142 | Query plan sql parameters → { plan, query: sql, parameters }
--------------------------------------------------------------------------------
/src/Droplet/Language/Internal/Condition.purs:
--------------------------------------------------------------------------------
1 | -- | Logical operators for filtering records
2 | -- |
3 | -- | Do not import this module directly, it will break your code and make it not type safe. Use the sanitized `Droplet.Language` instead
4 | module Droplet.Language.Internal.Condition (class ToCondition, class ValidComparision, OuterScope, In, class Comparison, Op(..), IsNotNull(..), isNotNull, IsNull(..), isNull, in_, and, Exists(..), Not(..), not, BinaryOperator(..), equals, notEquals, greaterThan, lesserThan, greaterEqualsThan, lesserEqualsThan, or, (.&&.), (.<>.), (.=.), (.||.), (.<.), (.>.), (.<=.), (.>=.)) where
5 |
6 | import Prelude
7 |
8 | import Data.Array.NonEmpty (NonEmptyArray)
9 | import Data.Maybe (Maybe(..))
10 | import Droplet.Language.Internal.Definition (class IsNullable, class ToValue, class UnwrapDefinition, class UnwrapNullable, Path)
11 | import Prim.Row (class Cons)
12 | import Prim.TypeError (class Fail, Text)
13 | import Type.Proxy (Proxy)
14 |
15 | data BinaryOperator
16 | = Equals
17 | | NotEquals
18 | | GreaterThan
19 | | LesserThan
20 | | GreaterEqualsThan
21 | | LesserEqualsThan
22 | | And
23 | | Or
24 |
25 | data In = In -- only for non empty arrays
26 |
27 | data Exists = Exists
28 |
29 | data Not = Not
30 |
31 | data IsNotNull = IsNotNull
32 |
33 | data IsNull = IsNull
34 |
35 | derive instance Eq BinaryOperator
36 |
37 | -- | Wrapper for comparisons
38 | data Op b c = Op (Maybe BinaryOperator) b c
39 |
40 | data OuterScope
41 |
42 | -- | SQL logical expressions
43 | class ToCondition (c ∷ Type) (fields ∷ Row Type) (alias ∷ Symbol)
44 |
45 | -- | AND/OR
46 | instance (ToCondition (Op a b) fields alias, ToCondition (Op c d) fields alias) ⇒ ToCondition (Op (Op a b) (Op c d)) fields alias
47 |
48 | -- | EXISTS
49 | else instance ToCondition (Op Exists b) fields alias
50 |
51 | -- | IS NULL
52 | else instance (Cons name t d fields, IsNullable t) ⇒ ToCondition (Op IsNull (Proxy name)) fields alias
53 |
54 | else instance (Cons name t d fields, IsNullable t) ⇒ ToCondition (Op IsNull (Path alias name)) fields alias
55 |
56 | else instance ToCondition (Op IsNull (Path table name)) fields alias
57 |
58 | -- | IS NOT NULL
59 | else instance (Cons name t d fields, IsNullable t) ⇒ ToCondition (Op IsNotNull (Proxy name)) fields alias
60 |
61 | else instance (Cons name t d fields, IsNullable t) ⇒ ToCondition (Op IsNotNull (Path alias name)) fields alias
62 |
63 | else instance ToCondition (Op IsNotNull (Path table name)) fields alias
64 |
65 | -- | NOT
66 | else instance ToCondition a fields alias ⇒ ToCondition (Op Not a) fields alias
67 |
68 | -- | IN values
69 | else instance ToCondition (Op a b) fields alias ⇒ ToCondition (Op In (Op a (NonEmptyArray b))) fields alias
70 |
71 | -- | Comparisons
72 | else instance
73 | ( Comparison a fields alias t
74 | , Comparison b fields alias u
75 | , ValidComparision t u
76 | ) ⇒
77 | ToCondition (Op a b) fields alias
78 |
79 | -- | Whether expression can be compared
80 | class Comparison (c ∷ Type) (fields ∷ Row Type) (alias ∷ Symbol) (t ∷ Type) | c → fields t
81 |
82 | instance
83 | ( Cons name t d fields
84 | , UnwrapDefinition t u
85 | , UnwrapNullable u v
86 | ) ⇒
87 | Comparison (Proxy name) fields alias v
88 |
89 | else instance
90 | ( Cons name t d fields
91 | , UnwrapDefinition t u
92 | , UnwrapNullable u v
93 | ) ⇒
94 | Comparison (Path alias name) fields alias v
95 |
96 | else instance Comparison (Path table name) fields alias OuterScope
97 |
98 | else instance Fail (Text "Comparisons must not be wrapped in Maybe") ⇒ Comparison (Maybe t) fields alias t
99 |
100 | else instance ToValue t ⇒ Comparison t fields alias t
101 |
102 | -- | Whether given types can be compared
103 | class ValidComparision (t ∷ Type) (u ∷ Type)
104 |
105 | instance ValidComparision t OuterScope
106 |
107 | else instance ValidComparision OuterScope t
108 |
109 | else instance ValidComparision t t
110 |
111 | equals ∷ ∀ field other. field → other → Op field other
112 | equals field other = Op (Just Equals) field other
113 |
114 | notEquals ∷ ∀ compared field. field → compared → Op field compared
115 | notEquals field compared = Op (Just NotEquals) field compared
116 |
117 | greaterThan ∷ ∀ compared field. field → compared → Op field compared
118 | greaterThan field compared = Op (Just GreaterThan) field compared
119 |
120 | lesserThan ∷ ∀ compared field. field → compared → Op field compared
121 | lesserThan field compared = Op (Just LesserThan) field compared
122 |
123 | greaterEqualsThan ∷ ∀ compared field. field → compared → Op field compared
124 | greaterEqualsThan field compared = Op (Just GreaterEqualsThan) field compared
125 |
126 | lesserEqualsThan ∷ ∀ compared field. field → compared → Op field compared
127 | lesserEqualsThan field compared = Op (Just LesserEqualsThan) field compared
128 |
129 | and ∷ ∀ a b c d. Op a b → Op c d → Op (Op a b) (Op c d)
130 | and first second = Op (Just And) first second
131 |
132 | or ∷ ∀ a b c d. Op a b → Op c d → Op (Op a b) (Op c d)
133 | or first second = Op (Just Or) first second
134 |
135 | in_ ∷ ∀ compared field. field → compared → Op In (Op field compared)
136 | in_ field compared = Op Nothing In (Op Nothing field compared)
137 |
138 | not ∷ ∀ compared field. Op field compared → Op Not (Op field compared)
139 | not a = Op Nothing Not a
140 |
141 | isNull ∷ ∀ field. field → Op IsNull field
142 | isNull field = Op Nothing IsNull field
143 |
144 | isNotNull ∷ ∀ field. field → Op IsNotNull field
145 | isNotNull field = Op Nothing IsNotNull field
146 |
147 | infix 4 notEquals as .<>.
148 | infix 4 equals as .=.
149 | infix 4 greaterThan as .>.
150 | infix 4 lesserThan as .<.
151 | infix 4 greaterEqualsThan as .>=.
152 | infix 4 lesserEqualsThan as .<=.
153 | infixl 3 and as .&&.
154 | infixl 2 or as .||.
155 |
--------------------------------------------------------------------------------
/src/Droplet/Language/Internal/Definition.js:
--------------------------------------------------------------------------------
1 | export function readInt(value) {
2 | var n = parseInt(value, 10);
3 |
4 | if (typeof n === 'number' &&
5 | n === n &&
6 | n !== Infinity &&
7 | value !== -Infinity &&
8 | Math.floor(n) === n &&
9 | Math.abs(n) <= Number.MAX_SAFE_INTEGER)
10 | return n;
11 | }
12 |
13 | export function showForeigner(f) {
14 | return f +'';
15 | }
--------------------------------------------------------------------------------
/src/Droplet/Language/Internal/Definition.purs:
--------------------------------------------------------------------------------
1 | -- | Definition of SQL columns types as well conversions from and to columns
2 | -- |
3 | -- | Do not import this module directly, it will break your code and make it not type safe. Use the sanitized `Droplet.Language` instead
4 | module Droplet.Language.Internal.Definition
5 | ( Empty
6 | , Identity
7 | , Default(..)
8 | , Star(..)
9 | , Table(..)
10 | , E(..)
11 | , Dot
12 | , Composite
13 | , Path
14 | , ForeignKey
15 | , Joined
16 | , PrimaryKey
17 | , Constraint
18 | , Unique
19 | , Column(..)
20 | , C
21 | , class ToType
22 | , class IsNullable
23 | , class UnwrapNullable
24 | , class FromValue
25 | , class ToParameters
26 | , class ToValue
27 | , class UnwrapDefinition
28 | , class AppendPath
29 | , star
30 | , toType
31 | , toParameters
32 | , fromValue
33 | , toValue
34 | , path
35 | , (...)
36 | ) where
37 |
38 | import Prelude
39 | import Prim hiding (Constraint)
40 |
41 | import Control.Monad.Except as CME
42 | import Data.Array ((:))
43 | import Data.Bifunctor as DB
44 | import Data.BigInt (BigInt)
45 | import Data.BigInt as DBT
46 | import Data.Date (Date)
47 | import Data.Date as DD
48 | import Data.DateTime (DateTime(..), Time(..))
49 | import Data.Either (Either(..))
50 | import Data.Either as DE
51 | import Data.Either as DET
52 | import Data.Enum as DEN
53 | import Data.Int as DI
54 | import Data.Maybe (Maybe(..))
55 | import Data.Nullable (Nullable)
56 | import Data.Nullable as DN
57 | import Data.Reflectable (class Reflectable)
58 | import Data.Reflectable as DR
59 | import Data.String (Pattern(..))
60 | import Data.String as DST
61 | import Data.Symbol (class IsSymbol)
62 | import Data.Traversable as DT
63 | import Data.Tuple (Tuple)
64 | import Data.Tuple.Nested ((/\))
65 | import Droplet.Language.Internal.Token (bigIntegerType, booleanType, dateTimeType, dateType, dotSymbol, integerType, numberType, stringType)
66 | import Foreign (Foreign)
67 | import Foreign as F
68 | import Prim.Row (class Cons)
69 | import Prim.RowList (RowList, Cons, Nil)
70 | import Prim.Symbol (class Append)
71 | import Record as R
72 | import Type.Proxy (Proxy(..))
73 |
74 | foreign import readInt ∷ Foreign → Nullable Int
75 | foreign import showForeigner ∷ Foreign → String
76 |
77 | -- | Marks the query end
78 | data E = E
79 |
80 | data C :: forall k. k -> Type -> Type
81 | data C n t
82 |
83 | type Empty = ""
84 |
85 | type Dot = "."
86 |
87 | data Composite (name :: Symbol)
88 |
89 | -- | A trick to mark left joined columns as nullable
90 | data Joined (t ∷ Type)
91 |
92 | data Star = Star
93 |
94 | star ∷ Star
95 | star = Star
96 |
97 | -- | GENERATED ALWAYS AS IDENTITY constraint
98 | data Identity
99 |
100 | data Default = Default
101 |
102 | data PrimaryKey
103 |
104 | data Unique
105 |
106 | data ForeignKey (field :: Symbol) (table :: Type)
107 |
108 | data Constraint ∷ ∀ n. n → Type → Type
109 | data Constraint name t
110 |
111 | data Column (t :: Type) (constraint :: Type) = Column
112 |
113 | data Table (name ∷ Symbol) (fields ∷ Row Type) = Table
114 |
115 | -- | Qualified columns (e.g, table.column)
116 | data Path (alias ∷ Symbol) (field ∷ Symbol) = Path
117 |
118 | path ∷ ∀ alias field path pathField. Append alias Dot path ⇒ Append path field pathField ⇒ Proxy alias → Proxy field → Path alias field
119 | path _ _ = Path
120 |
121 | infix 7 path as ...
122 |
123 | -- | Converts a PureScript value into Postgres
124 | class ToValue v where
125 | toValue ∷ v → Foreign
126 |
127 | instance ToValue Int where
128 | toValue = F.unsafeToForeign
129 |
130 | instance ToValue String where
131 | toValue = F.unsafeToForeign
132 |
133 | instance ToValue Default where
134 | toValue _ = F.unsafeToForeign $ DN.null
135 |
136 | instance ToValue Boolean where
137 | toValue = F.unsafeToForeign
138 |
139 | instance ToValue Number where
140 | toValue = F.unsafeToForeign
141 |
142 | instance ToValue a ⇒ ToValue (Maybe a) where
143 | toValue = case _ of
144 | Nothing → F.unsafeToForeign DN.null
145 | Just a → toValue a
146 |
147 | instance ToValue BigInt where
148 | toValue = F.unsafeToForeign <<< DBT.toString
149 |
150 | instance ToValue Date where
151 | toValue = F.unsafeToForeign <<< formatDate
152 |
153 | instance ToValue a ⇒ ToValue (Array a) where
154 | toValue = F.unsafeToForeign <<< map toValue
155 |
156 | instance ToValue DateTime where
157 | toValue (DateTime date (Time h m s ms)) = F.unsafeToForeign $ formatDate date <> "t" <> time <> "+0000"
158 | where
159 | time = show (DEN.fromEnum h) <> ":" <> show (DEN.fromEnum m) <> ":" <> show (DEN.fromEnum s) <> dotSymbol <> show (DEN.fromEnum ms)
160 |
161 | formatDate ∷ Date → String
162 | formatDate date = show y <> "-" <> show m <> "-" <> show d
163 | where
164 | y = DEN.fromEnum $ DD.year date
165 | m = DEN.fromEnum $ DD.month date
166 | d = DEN.fromEnum $ DD.day date
167 |
168 | -- | Converts a Postgres value into PureScript
169 | class FromValue t where
170 | fromValue ∷ Foreign → Either String t
171 |
172 | --sometimes node pg returns a string for integers
173 | -- this might arise out a invalid type definition on the users part;
174 | -- the number is actually a big int;
175 | -- something funky
176 | --in the first two cases, readInt returns null, as well in the latter if the string can't be parsed as an integer
177 | instance FromValue Int where
178 | fromValue i = case DN.toMaybe $ readInt i of
179 | Nothing → Left $ "Could not parse value as integer: " <> showForeigner i
180 | Just int → Right int
181 |
182 | instance FromValue String where
183 | fromValue = DB.lmap show <<< CME.runExcept <<< F.readString
184 |
185 | instance FromValue Boolean where
186 | fromValue = DB.lmap show <<< CME.runExcept <<< F.readBoolean
187 |
188 | instance FromValue Number where
189 | fromValue = DB.lmap show <<< CME.runExcept <<< F.readNumber
190 |
191 | --tricky, since pg might return empty string for select some_side_effect_function()
192 | instance FromValue Unit where
193 | fromValue _ = Right unit
194 |
195 | instance FromValue v ⇒ FromValue (Array v) where
196 | fromValue = DT.traverse fromValue <=< DB.lmap show <<< CME.runExcept <<< F.readArray
197 |
198 | instance FromValue BigInt where
199 | fromValue v = do
200 | i ← DB.lmap show <<< CME.runExcept $ F.readString v
201 | DET.note ("Could not parse big int from " <> i) $ DBT.fromString i
202 |
203 | instance FromValue v ⇒ FromValue (Maybe v) where
204 | fromValue v
205 | | F.isNull v = pure Nothing
206 | | otherwise = Just <$> fromValue v
207 |
208 | instance FromValue Date where
209 | fromValue v = do
210 | s ← DB.lmap show <<< CME.runExcept $ F.readString v
211 | parseDate s $ "ISO 8601 date parsing failed for value: " <> s
212 |
213 | instance FromValue DateTime where
214 | fromValue v = do
215 | s ← DB.lmap show <<< CME.runExcept $ F.readString v
216 | let errorMessage = "ISO 8601 date time parsing failed for value: " <> s
217 | case DST.split (Pattern " ") s of
218 | [ datePart, timePart ] → do
219 | date ← parseDate datePart errorMessage
220 | time ← parseTime timePart errorMessage
221 | Right $ DateTime date time
222 | _ → Left errorMessage
223 |
224 | parseDate ∷ String → String → Either String Date
225 | parseDate input errorMessage =
226 | case DST.split (Pattern "-") input of
227 | [ y, m, d ] → do
228 | let result = DD.canonicalDate <$> (DEN.toEnum =<< DI.fromString y) <*> (DEN.toEnum =<< DI.fromString m) <*> (DEN.toEnum =<< DI.fromString d)
229 | DE.note errorMessage result
230 | _ → Left errorMessage
231 |
232 | parseTime ∷ String → String → Either String Time
233 | parseTime input errorMessage =
234 | case DST.split (Pattern ":") input of
235 | [ h, m, s ] → do
236 | let result = Time <$> (DEN.toEnum =<< DI.fromString h) <*> (DEN.toEnum =<< DI.fromString m) <*> (DEN.toEnum =<< DI.fromString (DST.take 2 s)) <*> (DEN.toEnum 0)
237 | DE.note errorMessage result
238 | _ → Left errorMessage
239 |
240 | -- | Convenience to remove type wrappers
241 | class UnwrapDefinition (w ∷ Type) (t ∷ Type) | w → t
242 |
243 | instance UnwrapDefinition (Column t c) t
244 |
245 | else instance UnwrapDefinition t u ⇒ UnwrapDefinition (Joined t) u
246 |
247 | else instance UnwrapDefinition t t
248 |
249 | -- | Convenience to remove nullable wrappers
250 | class UnwrapNullable (w ∷ Type) (t ∷ Type) | w → t
251 |
252 | instance UnwrapNullable (Maybe t) t
253 |
254 | else instance UnwrapNullable t t
255 |
256 | class IsNullable (t ∷ Type)
257 |
258 | instance IsNullable (Column (Maybe t) c)
259 |
260 | instance IsNullable (Maybe t)
261 |
262 | instance IsNullable (Joined t)
263 |
264 | class ToParameters record (list ∷ RowList Type) where
265 | toParameters ∷ Proxy list → Record record → Array (Tuple String Foreign)
266 |
267 | instance ToParameters record Nil where
268 | toParameters _ _ = []
269 |
270 | instance
271 | ( IsSymbol name
272 | , Reflectable name String
273 | , ToValue t
274 | , Cons name t e record
275 | , ToParameters record rest
276 | ) ⇒
277 | ToParameters record (Cons name t rest) where
278 | toParameters _ record = (DR.reflectType name /\ toValue (R.get name record)) : toParameters (Proxy ∷ Proxy rest) record
279 | where
280 | name = Proxy ∷ Proxy name
281 |
282 | -- | Simplify append qualifying column names
283 | class AppendPath (alias ∷ Symbol) (name ∷ Symbol) (fullPath ∷ Symbol) | alias name → fullPath
284 |
285 | instance (Append alias Dot path, Append path name fullPath) ⇒ AppendPath alias name fullPath
286 |
287 | -- | How a value should be generated for DEFAULT and other constraints
288 | -- |
289 | -- | Required only if using migrations; other cases are handled by `ToValue`
290 | class ToConstraintValue (t ∷ Type) where
291 | toConstraintValue ∷ Proxy t → Foreign
292 |
293 |
294 | -- | String representation of field types
295 | class ToType (t ∷ Type) where
296 | toType ∷ Proxy t → String
297 |
298 | instance ToType Int where
299 | toType _ = integerType
300 |
301 | instance ToType BigInt where
302 | toType _ = bigIntegerType
303 |
304 | instance ToType Date where
305 | toType _ = dateType
306 |
307 | instance ToType DateTime where
308 | toType _ = dateTimeType
309 |
310 | instance ToType String where
311 | toType _ = stringType
312 |
313 | instance ToType Number where
314 | toType _ = numberType
315 |
316 | instance ToType Boolean where
317 | toType _ = booleanType
318 |
319 | instance ToType t ⇒ ToType (Maybe t) where
320 | toType _ = toType (Proxy ∷ _ t)
--------------------------------------------------------------------------------
/src/Droplet/Language/Internal/Function.purs:
--------------------------------------------------------------------------------
1 | module Droplet.Language.Internal.Function
2 | ( class TextColumn
3 | , count
4 | , class ToCount
5 | , random
6 | , Aggregate(..)
7 | , function'
8 | , string_agg
9 | , class ToStringAgg
10 | , class ToArrayAgg
11 | , class ToCoalesce
12 | , PgFunction(..)
13 | , array_agg
14 | , class MatchArgument
15 | , function
16 | , class MatchArgumentList
17 | , FunctionSignature
18 | , FunctionSignature'
19 | , coalesce
20 | ) where
21 |
22 | import Prelude
23 |
24 | import Data.BigInt (BigInt)
25 | import Data.Maybe (Maybe)
26 | import Data.Tuple.Nested (type (/\))
27 | import Droplet.Language.Internal.Definition (class AppendPath, class ToValue, class UnwrapDefinition, class UnwrapNullable, E, Path, Star)
28 | import Prim.Row (class Cons)
29 | import Type.Equality (class TypeEquals)
30 | import Type.Proxy (Proxy)
31 |
32 | -- fields parameter is needed to match later with ToProjection
33 | -- | Built-in aggregate functions
34 | data Aggregate input rest (fields ∷ Row Type) (output ∷ Type)
35 | = Count input
36 | | StringAgg input rest
37 | | ArrayAgg input
38 |
39 | -- | Declares a functions
40 | data PgFunction (input ∷ Type) args (fields ∷ Row Type) (output ∷ Type) = PgFunction String args
41 |
42 | type FunctionSignature input output = ∀ args fields. MatchArgumentList input args fields ⇒ args → PgFunction input args fields output
43 |
44 | type FunctionSignature' output = ∀ fields. PgFunction Void Unit fields output
45 |
46 | class ToCount (f ∷ Type) (fields ∷ Row Type) | f → fields
47 |
48 | instance Cons name t e fields ⇒ ToCount (Proxy name) fields
49 |
50 | instance Cons name t e fields ⇒ ToCount (Path alias name) fields
51 |
52 | instance ToCount Star fields
53 |
54 | class ToStringAgg (f ∷ Type) (rest ∷ Type) (fields ∷ Row Type) | f → fields
55 |
56 | instance (Cons name t e fields, TextColumn t) ⇒ ToStringAgg (Proxy name) String fields
57 |
58 | instance (Cons name t e fields, TextColumn t) ⇒ ToStringAgg (Path alias name) String fields
59 |
60 | -- | Extra class for clearer error messages
61 | class TextColumn (t ∷ Type)
62 |
63 | instance TextColumn String
64 |
65 | instance TextColumn (Maybe String)
66 |
67 | class ToArrayAgg (f ∷ Type) (fields ∷ Row Type) (t ∷ Type) | f → fields t
68 |
69 | instance (Cons name t e fields, UnwrapDefinition t u) ⇒ ToArrayAgg (Proxy name) fields u
70 |
71 | instance
72 | ( AppendPath alias name fullPath
73 | , Cons fullPath t e fields
74 | , UnwrapDefinition t u
75 | ) ⇒
76 | ToArrayAgg (Path alias name) fields u
77 |
78 | -- | Function arguments must match input type
79 | class MatchArgumentList (input ∷ Type) (args ∷ Type) (fields ∷ Row Type)
80 |
81 | instance (MatchArgumentList inp ar fields, MatchArgumentList ut gs fields) ⇒ MatchArgumentList (inp /\ ut) (ar /\ gs) fields
82 |
83 | else instance (MatchArgument i fields t, MatchArgument a fields t) ⇒ MatchArgumentList i a fields
84 |
85 | -- | coalesce arguments must be of same type
86 | class ToCoalesce (a ∷ Type) (fields ∷ Row Type) (t ∷ Type) | a → t
87 |
88 | instance (ToCoalesce inp fields t, ToCoalesce ut fields t) ⇒ ToCoalesce (inp /\ ut) fields t
89 |
90 | else instance MatchArgument i fields t ⇒ ToCoalesce i fields t
91 |
92 | class MatchArgument (a ∷ Type) (fields ∷ Row Type) (t ∷ Type) | a → t
93 |
94 | instance
95 | ( Cons name t d fields
96 | , UnwrapDefinition t u
97 | , UnwrapNullable u v
98 | ) ⇒
99 | MatchArgument (Proxy name) fields v
100 |
101 | else instance
102 | ( AppendPath alias name fullPath
103 | , Cons fullPath t d fields
104 | , UnwrapDefinition t u
105 | , UnwrapNullable u v
106 | ) ⇒
107 | MatchArgument (Path alias name) fields v
108 |
109 | else instance (UnwrapNullable o t, TypeEquals fd fields) ⇒ MatchArgument (PgFunction i a fd o) fields t
110 |
111 | else instance (ToValue a, UnwrapNullable a t) ⇒ MatchArgument a fields t
112 |
113 | count ∷ ∀ f fields. ToCount f fields ⇒ f → Aggregate f E fields BigInt
114 | count = Count
115 |
116 | --Maybe String because null
117 | string_agg ∷ ∀ f rest fields. ToStringAgg f rest fields ⇒ f → rest → Aggregate f rest fields (Maybe String)
118 | string_agg f rest = StringAgg f rest
119 |
120 | --Maybe t because null
121 | array_agg ∷ ∀ f t fields. ToArrayAgg f fields t ⇒ f → Aggregate f E fields (Maybe (Array t))
122 | array_agg f = ArrayAgg f
123 |
124 | random ∷ FunctionSignature' Number
125 | random = function' "random"
126 |
127 | coalesce ∷ ∀ input output fields. ToCoalesce input fields output ⇒ input → PgFunction input input fields (Maybe output)
128 | coalesce = PgFunction "coalesce"
129 |
130 | -- | Represents a function that takes arguments
131 | function ∷ ∀ input output. String → FunctionSignature input output
132 | function name args = PgFunction name args
133 |
134 | -- | Represents a function that takes no arguments
135 | function' ∷ ∀ output. String → FunctionSignature' output
136 | function' name = PgFunction name unit
--------------------------------------------------------------------------------
/src/Droplet/Language/Internal/Token.purs:
--------------------------------------------------------------------------------
1 | -- | SQL Keywords and tokens
2 | module Droplet.Language.Internal.Token where
3 |
4 | --magic strings
5 | selectKeyword ∷ String
6 | selectKeyword = "SELECT "
7 |
8 | distinctKeyword ∷ String
9 | distinctKeyword = "DISTINCT "
10 |
11 | isNullKeyword ∷ String
12 | isNullKeyword = " IS NULL"
13 |
14 | isNotNullKeyword ∷ String
15 | isNotNullKeyword = " IS NOT NULL"
16 |
17 | unionKeyword ∷ String
18 | unionKeyword = " UNION "
19 |
20 | allKeyword ∷ String
21 | allKeyword = "ALL "
22 |
23 | fromKeyword ∷ String
24 | fromKeyword = " FROM "
25 |
26 | constraintKeyword ∷ String
27 | constraintKeyword = "CONSTRAINT "
28 |
29 | identityKeyword ∷ String
30 | identityKeyword = " GENERATED ALWAYS AS IDENTITY"
31 |
32 | uniqueKeyword ∷ String
33 | uniqueKeyword = " UNIQUE"
34 |
35 | whereKeyword ∷ String
36 | whereKeyword = " WHERE "
37 |
38 | referencesKeyword ∷ String
39 | referencesKeyword = " REFERENCES "
40 |
41 | andKeyword ∷ String
42 | andKeyword = " AND "
43 |
44 | orKeyword ∷ String
45 | orKeyword = " OR "
46 |
47 | asKeyword ∷ String
48 | asKeyword = " AS "
49 |
50 | innerKeyword ∷ String
51 | innerKeyword = " INNER "
52 |
53 | primaryKeyKeyword ∷ String
54 | primaryKeyKeyword = " PRIMARY KEY"
55 |
56 | foreignKeyKeyword ∷ String
57 | foreignKeyKeyword = " FOREIGN KEY"
58 |
59 | defaultKeyword ∷ String
60 | defaultKeyword = "DEFAULT"
61 |
62 | joinKeyword ∷ String
63 | joinKeyword = "JOIN "
64 |
65 | onKeyword ∷ String
66 | onKeyword = " ON "
67 |
68 | groupByKeyword ∷ String
69 | groupByKeyword = " GROUP BY "
70 |
71 | existsKeyword ∷ String
72 | existsKeyword = "EXISTS "
73 |
74 | inKeyword ∷ String
75 | inKeyword = " IN "
76 |
77 | notKeyword ∷ String
78 | notKeyword = "NOT "
79 |
80 | leftKeyword ∷ String
81 | leftKeyword = " LEFT "
82 |
83 | starSymbol ∷ String
84 | starSymbol = "*"
85 |
86 | comma ∷ String
87 | comma = ", "
88 |
89 | openBracket ∷ String
90 | openBracket = "("
91 |
92 | closeBracket ∷ String
93 | closeBracket = ")"
94 |
95 | equalsSymbol ∷ String
96 | equalsSymbol = " = "
97 |
98 | notEqualsSymbol ∷ String
99 | notEqualsSymbol = " <> "
100 |
101 | lesserThanSymbol ∷ String
102 | lesserThanSymbol = " < "
103 |
104 | greaterThanSymbol ∷ String
105 | greaterThanSymbol = " > "
106 |
107 | lesserEqualsThanSymbol ∷ String
108 | lesserEqualsThanSymbol = " <= "
109 |
110 | greaterEqualsThanSymbol ∷ String
111 | greaterEqualsThanSymbol = " >= "
112 |
113 | parameterSymbol ∷ String
114 | parameterSymbol = "$"
115 |
116 | insertKeyword ∷ String
117 | insertKeyword = "INSERT INTO "
118 |
119 | valuesKeyword ∷ String
120 | valuesKeyword = " VALUES"
121 |
122 | updateKeyword ∷ String
123 | updateKeyword = "UPDATE "
124 |
125 | setKeyword ∷ String
126 | setKeyword = " SET "
127 |
128 | deleteKeyword ∷ String
129 | deleteKeyword = "DELETE"
130 |
131 | atSymbol ∷ String
132 | atSymbol = "@"
133 |
134 | returningKeyword ∷ String
135 | returningKeyword = " RETURNING "
136 |
137 | descKeyword ∷ String
138 | descKeyword = " DESC"
139 |
140 | ascKeyword ∷ String
141 | ascKeyword = " ASC"
142 |
143 | orderKeyword ∷ String
144 | orderKeyword = " ORDER "
145 |
146 | byKeyword ∷ String
147 | byKeyword = "BY "
148 |
149 | countFunctionName ∷ String
150 | countFunctionName = "count"
151 |
152 | limitKeyword ∷ String
153 | limitKeyword = " LIMIT "
154 |
155 | offsetKeyword ∷ String
156 | offsetKeyword = " OFFSET "
157 |
158 | semicolon ∷ String
159 | semicolon = ";"
160 |
161 | dotSymbol ∷ String
162 | dotSymbol = "."
163 |
164 | string_aggFunctionName ∷ String
165 | string_aggFunctionName = "string_agg"
166 |
167 | array_aggFunctionName ∷ String
168 | array_aggFunctionName = "array_agg"
169 |
170 | simpleQuoteSymbol ∷ String
171 | simpleQuoteSymbol = "'"
172 |
173 | integerType ∷ String
174 | integerType = "INTEGER"
175 |
176 | bigIntegerType ∷ String
177 | bigIntegerType = "BIGINT"
178 |
179 | dateType ∷ String
180 | dateType = "DATE"
181 |
182 | tableKeyword ∷ String
183 | tableKeyword = "TABLE "
184 |
185 | notNullKeyword ∷ String
186 | notNullKeyword = " NOT NULL"
187 |
188 | dateTimeType ∷ String
189 | dateTimeType = "TIMESTAMPTZ"
190 |
191 | stringType ∷ String
192 | stringType = "TEXT"
193 |
194 | booleanType ∷ String
195 | booleanType = "BOOL"
196 |
197 | addKeyword ∷ String
198 | addKeyword = "ADD "
199 |
200 | alterKeyword ∷ String
201 | alterKeyword = "ALTER "
202 |
203 | dropKeyword ∷ String
204 | dropKeyword = "DROP "
205 |
206 | numberType ∷ String
207 | numberType = "DOUBLE PRECISION"
208 |
209 | createKeyword ∷ String
210 | createKeyword = "CREATE "
211 |
212 | newline ∷ String
213 | newline = "\n"
214 |
215 | space ∷ String
216 | space = " "
217 |
218 | quoteSymbol ∷ String
219 | quoteSymbol = """""""
220 |
--------------------------------------------------------------------------------
/test/Alter.purs:
--------------------------------------------------------------------------------
1 | module Test.Alter where
2 |
3 | import Droplet.Language
4 | import Prelude hiding (add)
5 | import Prim hiding (Constraint)
6 | import Test.Types
7 |
8 | import Data.BigInt (BigInt)
9 | import Data.Date (Date)
10 | import Data.DateTime (DateTime)
11 | import Data.Maybe (Maybe)
12 | import Data.Tuple.Nested (type (/\))
13 | import Droplet.Driver as DD
14 | import Droplet.Language.Internal.Translate as DLIQ
15 | import Effect.Class (liftEffect)
16 | import Test.Model (connectionInfo)
17 | import Test.Model as TM
18 | import Test.Spec (Spec)
19 | import Test.Spec as TS
20 | import Type.Proxy (Proxy(..))
21 |
22 | tests ∷ Spec Unit
23 | tests = TS.describe "alter" do
24 | TS.describe "table" do
25 | TS.describe "add" do
26 | TS.it "no constraint" do
27 | let test = Table ∷ Table "test" (id ∷ Int)
28 | pool ← liftEffect $ DD.newPool connectionInfo
29 | void $ DD.withTransaction pool $ \c → DD.execute c $ create # table test
30 |
31 | let q = alter # table test # add name (Proxy :: _ String)
32 | TM.notParameterized """ALTER TABLE "test" ADD "name" TEXT NOT NULL""" $ DLIQ.buildQuery q
33 | void $ TM.resultOnly q
34 | TS.describe "constraint" do
35 | TS.it "simple" do
36 | let test = Table ∷ Table "test" (id ∷ Int)
37 | pool ← liftEffect $ DD.newPool connectionInfo
38 | void $ DD.withTransaction pool $ \c → DD.execute c $ create # table test
39 |
40 | let q = alter # table test # add name (Column :: Column String Unique)
41 | TM.notParameterized """ALTER TABLE "test" ADD "name" TEXT NOT NULL UNIQUE""" $ DLIQ.buildQuery q
42 | void $ TM.resultOnly q
43 | TS.it "named" do
44 | let test = Table ∷ Table "test" (id ∷ Int)
45 | pool ← liftEffect $ DD.newPool connectionInfo
46 | void $ DD.withTransaction pool $ \c → DD.execute c $ create # table test
47 |
48 | let q = alter # table test # add name (Column :: Column String (Constraint "named" Unique))
49 | TM.notParameterized """ALTER TABLE "test" ADD "name" TEXT NOT NULL CONSTRAINT "named" UNIQUE""" $ DLIQ.buildQuery q
50 | void $ TM.resultOnly q
--------------------------------------------------------------------------------
/test/As.purs:
--------------------------------------------------------------------------------
1 | module Test.As where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types (b, id, messages, n, name, t, u, users)
6 |
7 | import Data.Maybe (Maybe(..))
8 | import Droplet.Language.Internal.Translate as DLIQ
9 | import Test.Model as TM
10 |
11 | import Test.Spec (Spec)
12 | import Test.Spec as TS
13 | import Type.Proxy (Proxy(..))
14 |
15 | tests ∷ Spec Unit
16 | tests =
17 | TS.describe "as" do
18 | TS.describe "named field" do
19 | TS.it "casing" do
20 | let q = select (id # as (Proxy ∷ Proxy "AbCd")) # from users
21 | TM.notParameterized """SELECT "id" AS "AbCd" FROM "users"""" $ DLIQ.buildQuery q
22 | TM.result q [ { "AbCd": 1 }, { "AbCd": 2 } ]
23 | TS.describe "named table" do
24 | TS.it "field" do
25 | let q = select id # from (users # as u)
26 | TM.notParameterized """SELECT "id" FROM "users" AS "u"""" $ DLIQ.buildQuery q
27 | TM.result q [ { id: 1 }, { id: 2 } ]
28 | TS.it "named aliased field" do
29 | let q = select (u ... id # as id) # from (users # as u)
30 | TM.notParameterized """SELECT "u"."id" AS "id" FROM "users" AS "u"""" $ DLIQ.buildQuery q
31 | TM.result q [ { id: 1 }, { id: 2 } ]
32 | TS.describe "named queries" do
33 | TS.it "subquery column" do
34 | let q = select (select id # from users # wher (name .=. "mary") # orderBy id # limit (Proxy :: _ 1) # as b) # from users # wher (id .=. 1 .||. id .=. 2)
35 | TM.parameterized """SELECT (SELECT "id" FROM "users" WHERE "name" = $1 ORDER BY "id" LIMIT 1) AS "b" FROM "users" WHERE ("id" = $2 OR "id" = $3)""" $ DLIQ.buildQuery q
36 | TM.result q [ { b: Just 2 }, { b: Just 2 } ]
37 | TS.it "scalar" do
38 | let q = select (4 # as n) # from (select (4 # as n) # from users # wher (id .=. id) # as u)
39 | TM.notParameterized """SELECT 4 AS "n" FROM (SELECT 4 AS "n" FROM "users" WHERE "id" = "id") AS "u"""" $ DLIQ.buildQuery q
40 | TM.result q [ { n: 4 }, { n: 4 } ]
41 | TS.it "field" do
42 | let q = select id # from (select id # from messages # wher (id .=. id) # as t)
43 | TM.notParameterized """SELECT "id" FROM (SELECT "id" FROM "messages" WHERE "id" = "id") AS "t"""" $ DLIQ.buildQuery q
44 | TM.result q [ { id: 1 }, { id: 2 } ]
--------------------------------------------------------------------------------
/test/Create.purs:
--------------------------------------------------------------------------------
1 | module Test.Create where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Data.BigInt (BigInt)
8 | import Prim hiding (Constraint)
9 | import Data.Date (Date)
10 | import Data.DateTime (DateTime)
11 | import Data.Maybe (Maybe)
12 | import Data.Tuple.Nested (type (/\))
13 | import Droplet.Language.Internal.Translate as DLIQ
14 | import Test.Model as TM
15 | import Test.Spec (Spec)
16 | import Test.Spec as TS
17 |
18 | tests ∷ Spec Unit
19 | tests =
20 | TS.describe "create" do
21 | pure unit
22 | TS.describe "table" do
23 | TS.it "plain types" do
24 | let q = create # table (Table ∷ Table "test" (id ∷ Int, name ∷ String, set ∷ Boolean, n ∷ Number, bigId ∷ BigInt, date ∷ Date, dateTime ∷ DateTime))
25 | TM.notParameterized """CREATE TABLE "test" ("bigId" BIGINT NOT NULL, "date" DATE NOT NULL, "dateTime" TIMESTAMPTZ NOT NULL, "id" INTEGER NOT NULL, "n" DOUBLE PRECISION NOT NULL, "name" TEXT NOT NULL, "set" BOOL NOT NULL)""" $ DLIQ.buildQuery q
26 | void $ TM.resultOnly q
27 | TS.it "maybe type" do
28 | let q = create # table (Table ∷ Table "test" (id ∷ Maybe Int, name ∷ String))
29 | TM.notParameterized """CREATE TABLE "test" ("id" INTEGER, "name" TEXT NOT NULL)""" $ DLIQ.buildQuery q
30 | void $ TM.resultOnly q
31 | TS.describe "constraints" do
32 | TS.it "single" do
33 | let q = create # table (Table ∷ Table "test" (id ∷ Maybe Int, name ∷ String, c ∷ Column Int Identity))
34 | TM.notParameterized """CREATE TABLE "test" ("c" INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY, "id" INTEGER, "name" TEXT NOT NULL)""" $ DLIQ.buildQuery q
35 | void $ TM.resultOnly q
36 | TS.it "named" do
37 | let q = create # table (Table ∷ Table "test" (id ∷ Maybe Int, name ∷ String, c ∷ Column Int (Constraint "pk" Identity)))
38 | TM.notParameterized """CREATE TABLE "test" ("c" INTEGER NOT NULL CONSTRAINT "pk" GENERATED ALWAYS AS IDENTITY, "id" INTEGER, "name" TEXT NOT NULL)""" $ DLIQ.buildQuery q
39 | void $ TM.resultOnly q
40 | TS.it "many" do
41 | let q = create # table (Table ∷ Table "test" (id ∷ Maybe Int, d ∷ Column String (PrimaryKey /\ Unique), name ∷ String, c ∷ Column Int Identity))
42 | TM.notParameterized """CREATE TABLE "test" ("c" INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY, "d" TEXT NOT NULL PRIMARY KEY UNIQUE, "id" INTEGER, "name" TEXT NOT NULL)""" $ DLIQ.buildQuery q
43 | void $ TM.resultOnly q
44 | TS.it "foreign key" do
45 | let q = create # table (Table ∷ Table "test" (id ∷ Maybe Int, fk ∷ Column Int (ForeignKey "id" UsersTable)))
46 | TM.notParameterized """CREATE TABLE "test" ("fk" INTEGER NOT NULL REFERENCES "users"("id"), "id" INTEGER)""" $ DLIQ.buildQuery q
47 | void $ TM.resultOnly q
48 | TS.describe "composite" do
49 | TS.it "primary key" do
50 | let q = create # table (Table ∷ Table "test" (c ∷ Column Int (Constraint (Composite "pk") PrimaryKey), d ∷ Column Int (Constraint (Composite "pk") PrimaryKey)))
51 | TM.notParameterized """CREATE TABLE "test" ("c" INTEGER NOT NULL, "d" INTEGER NOT NULL, CONSTRAINT "pk" PRIMARY KEY("c", "d"))""" $ DLIQ.buildQuery q
52 | void $ TM.resultOnly q
53 | TS.it "foreign key" do
54 | let q = create # table (Table ∷ Table "test" (id ∷ Maybe Int, fk1 ∷ Column Int (Constraint (Composite "fk") (ForeignKey "id" DoublePrimaryKeyTable)), fk2 ∷ Column Int (Constraint (Composite "fk") (ForeignKey "second_id" DoublePrimaryKeyTable))))
55 | TM.notParameterized """CREATE TABLE "test" ("fk1" INTEGER NOT NULL, "fk2" INTEGER NOT NULL, "id" INTEGER, CONSTRAINT "fk" FOREIGN KEY("fk1", "fk2") REFERENCES "double_primary_key"("id", "second_id"))""" $ DLIQ.buildQuery q
56 |
--------------------------------------------------------------------------------
/test/Delete.purs:
--------------------------------------------------------------------------------
1 | module Test.Delete where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Droplet.Language.Internal.Translate as DLIQ
8 | import Test.Model as TM
9 |
10 | import Test.Spec (Spec)
11 | import Test.Spec as TS
12 |
13 |
14 | tests ∷ Spec Unit
15 | tests = do
16 | TS.describe "delete" do
17 | TS.it "all" do
18 | let q = delete # from users
19 | TM.notParameterized """DELETE FROM "users"""" $ DLIQ.buildQuery q
20 | TM.result' q []
21 | TS.it "where" do
22 | let q = delete # from users # wher (id .=. 3)
23 | TM.parameterized """DELETE FROM "users" WHERE "id" = $1""" $ DLIQ.buildQuery q
24 | TM.result' q []
25 |
--------------------------------------------------------------------------------
/test/Distinct.purs:
--------------------------------------------------------------------------------
1 | module Test.Distinct where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 | import Data.Tuple.Nested ((/\))
7 | import Droplet.Language.Internal.Translate as DLIQ
8 | import Test.Model as TM
9 |
10 | import Test.Spec (Spec)
11 | import Test.Spec as TS
12 |
13 |
14 | tests ∷ Spec Unit
15 | tests =
16 | TS.describe "distinct" do
17 | TS.it "field" do
18 | let q = select (distinct id) # from messages
19 | TM.notParameterized """SELECT DISTINCT "id" FROM "messages"""" $ DLIQ.buildQuery q
20 | TM.result' q []
21 | TS.it "fields" do
22 | let q = select (distinct (id /\ name)) # from users
23 | TM.notParameterized """SELECT DISTINCT "id", "name" FROM "users"""" $ DLIQ.buildQuery q
24 | TM.result q [ { id: 1, name: "josh" }, { id: 2, name: "mary" } ]
25 | TS.it "qualified fields" do
26 | let q = select (distinct (u ... id /\ u ... name)) # from (users # as u)
27 | TM.notParameterized """SELECT DISTINCT "u"."id" "u.id", "u"."name" "u.name" FROM "users" AS "u"""" $ DLIQ.buildQuery q
28 | TM.result q [ { "u.id": 1, "u.name": "josh" }, { "u.id": 2, "u.name": "mary" } ]
29 |
--------------------------------------------------------------------------------
/test/Drop.purs:
--------------------------------------------------------------------------------
1 | module Test.Drop where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Prim hiding (Constraint)
6 | import Test.Types
7 |
8 | import Data.Maybe (Maybe(..))
9 | import Droplet.Driver as DD
10 | import Droplet.Driver.Unsafe as DDU
11 | import Droplet.Language.Internal.Translate as DLIQ
12 | import Effect.Class (liftEffect)
13 | import Test.Model (connectionInfo)
14 | import Test.Model as TM
15 | import Test.Spec (Spec)
16 | import Test.Spec as TS
17 |
18 | tests ∷ Spec Unit
19 | tests =
20 | TS.describe "drop" do
21 | TS.it "table" do
22 | pool ← liftEffect $ DD.newPool connectionInfo
23 | void $ DD.withTransaction pool $ \c → DDU.unsafeExecute c Nothing "CREATE TABLE test (id INTEGER)" {}
24 | let q = drop # table (Table :: Table "test" (id :: Int))
25 | TM.notParameterized """DROP TABLE "test"""" $ DLIQ.buildQuery q
26 | void $ TM.resultOnly q
--------------------------------------------------------------------------------
/test/From.purs:
--------------------------------------------------------------------------------
1 | module Test.From where
2 |
3 | import Droplet.Language
4 | import Prelude hiding (join)
5 | import Test.Types
6 |
7 | import Data.Maybe (Maybe(..))
8 | import Data.Tuple.Nested ((/\))
9 | import Droplet.Language.Internal.Translate as DLIQ
10 | import Test.Model as TM
11 |
12 | import Test.Spec (Spec)
13 | import Test.Spec as TS
14 | import Type.Proxy (Proxy(..))
15 |
16 |
17 | tests ∷ Spec Unit
18 | tests = do
19 | TS.describe "from" do
20 | TS.it "star" do
21 | let q = select star # from messages
22 | TM.notParameterized """SELECT * FROM "messages"""" $ DLIQ.buildQuery q
23 | void $ TM.resultOnly q
24 | TS.it "null fields" do
25 | let q = select (created /\ _by) # from tags
26 | TM.notParameterized """SELECT "created", "by" FROM "tags"""" $ DLIQ.buildQuery q
27 | TM.result q [ { created: Nothing, by: Just 1 } ]
28 | TS.describe "named table" do
29 | TS.it "path" do
30 | let q = select (u ... id) # from (users # as u)
31 | TM.notParameterized """SELECT "u"."id" "u.id" FROM "users" AS "u"""" $ DLIQ.buildQuery q
32 | TM.result q [ { "u.id": 1 }, { "u.id": 2 } ]
33 | TS.it "aliased path" do
34 | let q = select (u ... id # as id) # from (users # as u)
35 | TM.notParameterized """SELECT "u"."id" AS "id" FROM "users" AS "u"""" $ DLIQ.buildQuery q
36 | TM.result q [ { id: 1 }, { id: 2 } ]
37 | TS.describe "named queries" do
38 | TS.it "star" do
39 | let q = select star # from (select (4 # as n) # from messages # as n)
40 | TM.notParameterized """SELECT * FROM (SELECT 4 AS "n" FROM "messages") AS "n"""" $ DLIQ.buildQuery q
41 | TM.result q [ { n: 4 }, { n: 4 } ]
42 | TS.it "star qualified qualified field" do
43 | let q = select star # from (select (bigB ... birthday) # from (users # as bigB) # as t)
44 | TM.notParameterized """SELECT * FROM (SELECT "B"."birthday" "B.birthday" FROM "users" AS "B") AS "t"""" $ DLIQ.buildQuery q
45 | TM.result q [ { "B.birthday": TM.makeDate 1990 1 1 }, { "B.birthday": TM.makeDate 1900 11 11 } ]
46 | TS.it "star bracket" do
47 | let q = select star # from (select (select id # from users # orderBy id # limit (Proxy :: _ 1)) # from users # as n)
48 | TM.notParameterized """SELECT * FROM (SELECT (SELECT "id" FROM "users" ORDER BY "id" LIMIT 1) FROM "users") AS "n"""" $ DLIQ.buildQuery q
49 | TM.result q [ { id: Just 1 }, { id: Just 1 } ]
50 | TS.it "field" do
51 | let q = select birthday # from (select birthday # from users # as t)
52 | TM.notParameterized """SELECT "birthday" FROM (SELECT "birthday" FROM "users") AS "t"""" $ DLIQ.buildQuery q
53 | TM.result q [ { birthday: TM.makeDate 1990 1 1 }, { birthday: TM.makeDate 1900 11 11 } ]
54 | TS.it "qualified field" do
55 | let q = select (t ... birthday) # from (select birthday # from users # as t)
56 | TM.notParameterized """SELECT "t"."birthday" "t.birthday" FROM (SELECT "birthday" FROM "users") AS "t"""" $ DLIQ.buildQuery q
57 | TM.result q [ { "t.birthday": TM.makeDate 1990 1 1 }, { "t.birthday": TM.makeDate 1900 11 11 } ]
58 | TS.it "qualified qualified field" do
59 | let q = select (t ... (Proxy ∷ Proxy "B.birthday")) # from (select (bigB ... birthday) # from (users # as bigB) # as t)
60 | TM.notParameterized """SELECT "t"."B.birthday" "t.B.birthday" FROM (SELECT "B"."birthday" "B.birthday" FROM "users" AS "B") AS "t"""" $ DLIQ.buildQuery q
61 | TM.result q [ { "t.B.birthday": TM.makeDate 1990 1 1 }, { "t.B.birthday": TM.makeDate 1900 11 11 } ]
62 | TS.it "renamed field" do
63 | let q = select t # from (select (birthday # as t) # from users # as t)
64 | TM.notParameterized """SELECT "t" FROM (SELECT "birthday" AS "t" FROM "users") AS "t"""" $ DLIQ.buildQuery q
65 | TM.result q [ { t: TM.makeDate 1990 1 1 }, { t: TM.makeDate 1900 11 11 } ]
66 | TS.it "join" do
67 | let q = select star # from (select (u ... name /\ t ... id) # from (join (users # as u) (messages # as t) # on (u ... id .=. t ... id)) # as b)
68 | TM.notParameterized """SELECT * FROM (SELECT "u"."name" "u.name", "t"."id" "t.id" FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id") AS "b"""" $ DLIQ.buildQuery q
69 | TM.result q [ { "t.id": 1, "u.name": "josh" }, { "t.id": 2, "u.name": "mary" } ]
70 |
--------------------------------------------------------------------------------
/test/Function.purs:
--------------------------------------------------------------------------------
1 | module Test.Function where
2 |
3 | import Prelude hiding (join)
4 |
5 | import Data.BigInt as DB
6 | import Data.Maybe (Maybe(..))
7 | import Data.Tuple.Nested ((/\))
8 | import Droplet.Language
9 | import Droplet.Language.Internal.Translate as DLIQ
10 | import Test.Model as TM
11 | import Test.Types
12 |
13 | import Test.Spec (Spec)
14 | import Test.Spec as TS
15 |
16 | tests ∷ Spec Unit
17 | tests = TS.describe "functions" do
18 | TS.describe "user defined functions" do
19 | TS.it "value" do
20 | let q = select (date_part_age ("year" /\ (TM.makeDateTime 1900 2 2)) # as u) # from users
21 | TM.parameterized """SELECT date_part_age($1, $2) AS "u" FROM "users"""" $ DLIQ.buildQuery q
22 | void $ TM.resultOnly q
23 | TS.it "field" do
24 | let q = select (date_part_age ("month" /\ date) # as u) # from messages
25 | TM.parameterized """SELECT date_part_age($1, "date") AS "u" FROM "messages"""" $ DLIQ.buildQuery q
26 | void $ TM.resultOnly q
27 | TS.describe "coalesce" do
28 | TS.it "scalar" do
29 | let q = select (coalesce (3 /\ 4) # as u) # from users
30 | TM.parameterized """SELECT coalesce($1, $2) AS "u" FROM "users"""" $ DLIQ.buildQuery q
31 | TM.result q [ { u: Just 3 }, { u: Just 3 } ]
32 | TS.it "field" do
33 | let q = select (coalesce (_by /\ _by) # as u) # from tags
34 | TM.notParameterized """SELECT coalesce("by", "by") AS "u" FROM "tags"""" $ DLIQ.buildQuery q
35 | TM.result q [ { u: Just 1 } ]
36 | TS.it "path" do
37 | let q = select (coalesce (u ... id /\ u ... id) # as u) # from (users # as u)
38 | TM.notParameterized """SELECT coalesce("u"."id", "u"."id") AS "u" FROM "users" AS "u"""" $ DLIQ.buildQuery q
39 | TM.result q [ { u: Just 1 }, { u: Just 2 } ]
40 | TS.it "mixed" do
41 | let q = select (coalesce (id /\ u ... id /\ 4) # as u) # from (users # as u)
42 | TM.parameterized """SELECT coalesce("id", "u"."id", $1) AS "u" FROM "users" AS "u"""" $ DLIQ.buildQuery q
43 | TM.result q [ { u: Just 1 }, { u: Just 2 } ]
44 | TS.it "function as argument" do
45 | let q = select (date_part_age ("year" /\ coalesce (date /\ utc_now)) # as u) # from messages
46 | TM.parameterized """SELECT date_part_age($1, coalesce("date", utc_now())) AS "u" FROM "messages"""" $ DLIQ.buildQuery q
47 | void $ TM.resultOnly q
48 | TS.describe "count" do
49 | TS.it "star" do
50 | let q = select (count star # as u) # from users
51 | TM.notParameterized """SELECT count(*) AS "u" FROM "users"""" $ DLIQ.buildQuery q
52 | TM.result q [ { u: DB.fromInt 2 } ]
53 | TS.it "field" do
54 | let q = select (count id # as u) # from users
55 | TM.notParameterized """SELECT count("id") AS "u" FROM "users"""" $ DLIQ.buildQuery q
56 | TM.result q [ { u: DB.fromInt 2 } ]
57 | TS.it "path" do
58 | let q = select (count (u ... id) # as u) # from (users # as u)
59 | TM.notParameterized """SELECT count("u"."id") AS "u" FROM "users" AS "u"""" $ DLIQ.buildQuery q
60 | TM.result q [ { u: DB.fromInt 2 } ]
61 | TS.it "function without parameters" do
62 | let q = select ((random # as u) /\ id) # from users
63 | TM.notParameterized """SELECT random() AS "u", "id" FROM "users"""" $ DLIQ.buildQuery q
64 | void $ TM.resultOnly q
65 | TS.describe "string_agg" do
66 | TS.it "field" do
67 | let q = select (string_agg name ", " # as u) # from users
68 | TM.parameterized """SELECT string_agg("name", $1) AS "u" FROM "users"""" $ DLIQ.buildQuery q
69 | TM.result q [ { u: Just "josh, mary" } ]
70 | TS.it "path" do
71 | let q = select (string_agg (u ... name) ", " # as u) # from (users # as u)
72 | TM.parameterized """SELECT string_agg("u"."name", $1) AS "u" FROM "users" AS "u"""" $ DLIQ.buildQuery q
73 | TM.result q [ { u: Just "josh, mary" } ]
74 | TS.describe "order by" do
75 | TS.it "field" do
76 | let q = select (string_agg name (", " # orderBy id) # as u) # from users
77 | TM.parameterized """SELECT string_agg("name", $1 ORDER BY "id") AS "u" FROM "users"""" $ DLIQ.buildQuery q
78 | TM.result q [ { u: Just "josh, mary" } ]
79 | TS.it "path" do
80 | let q = select (string_agg (u ... name) (", " # orderBy (u ... id)) # as u) # from (users # as u)
81 | TM.parameterized """SELECT string_agg("u"."name", $1 ORDER BY "u"."id") AS "u" FROM "users" AS "u"""" $ DLIQ.buildQuery q
82 | TM.result q [ { u: Just "josh, mary" } ]
83 | TS.describe "int_agg" do
84 | TS.it "field" do
85 | let q = select (array_agg name # as u) # from users
86 | TM.notParameterized """SELECT array_agg("name") AS "u" FROM "users"""" $ DLIQ.buildQuery q
87 | TM.result q [ { u: Just [ "josh", "mary" ] } ]
88 | TS.it "path" do
89 | let q = select (array_agg (u ... id) # as u) # from (users # as u)
90 | TM.notParameterized """SELECT array_agg("u"."id") AS "u" FROM "users" AS "u"""" $ DLIQ.buildQuery q
91 | TM.result q [ { u: Just [1, 2] } ]
92 | TS.it "joined source" do
93 | let q = select (array_agg (u ... id) # as u) # from (join (users # as u) (messages # as t) # on (u ... id .=. t ... id))
94 | TM.notParameterized """SELECT array_agg("u"."id") AS "u" FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
95 | TM.result q [ { u: Just [1, 2] } ]
96 | TS.describe "order by" do
97 | TS.it "field" do
98 | let q = select (array_agg (id # orderBy id) # as u) # from users
99 | TM.notParameterized """SELECT array_agg("id" ORDER BY "id") AS "u" FROM "users"""" $ DLIQ.buildQuery q
100 | TM.result q [ { u: Just [1, 2]} ]
101 | TS.it "path" do
102 | let q = select (array_agg (u ... name # orderBy (u ... id)) # as u) # from (users # as u)
103 | TM.notParameterized """SELECT array_agg("u"."name" ORDER BY "u"."id") AS "u" FROM "users" AS "u"""" $ DLIQ.buildQuery q
104 | TM.result q [ { u: Just ["josh", "mary"] } ]
105 |
--------------------------------------------------------------------------------
/test/GroupBy.purs:
--------------------------------------------------------------------------------
1 | module Test.GroupBy where
2 |
3 | import Droplet.Language (as, count, from, groupBy, orderBy, select, (...))
4 | import Prelude
5 | import Test.Types (b, id, name, u, users)
6 |
7 | import Data.BigInt as BG
8 | import Data.Tuple.Nested ((/\))
9 | import Droplet.Language.Internal.Translate as DLIQ
10 | import Test.Model as TM
11 |
12 | import Test.Spec (Spec)
13 | import Test.Spec as TS
14 |
15 |
16 | tests ∷ Spec Unit
17 | tests =
18 | TS.describe "group by" do
19 | TS.it "single field" do
20 | let q = select id # from users # groupBy id # orderBy id
21 | TM.notParameterized """SELECT "id" FROM "users" GROUP BY "id" ORDER BY "id"""" $ DLIQ.buildQuery q
22 | TM.result q [ { id: 1 }, { id: 2 } ]
23 | TS.it "many fields" do
24 | let q = select ((count id # as b) /\ name) # from users # groupBy (id /\ name) # orderBy id
25 | TM.notParameterized """SELECT count("id") AS "b", "name" FROM "users" GROUP BY "id", "name" ORDER BY "id"""" $ DLIQ.buildQuery q
26 | TM.result q [ { b: BG.fromInt 1, name: "josh" }, { b: BG.fromInt 1, name: "mary" } ]
27 | TS.describe "path" do
28 | TS.it "single field" do
29 | let q = select id # from (select id # from users # as u) # groupBy (u ... id) # orderBy id
30 | TM.notParameterized """SELECT "id" FROM (SELECT "id" FROM "users") AS "u" GROUP BY "u"."id" ORDER BY "id"""" $ DLIQ.buildQuery q
31 | TM.result q [ { id: 1 }, { id: 2 } ]
32 | TS.it "many fields" do
33 | let q = select (id /\ u ... name) # from (users # as u) # groupBy (u ... name /\ id) # orderBy id
34 | TM.notParameterized """SELECT "id", "u"."name" "u.name" FROM "users" AS "u" GROUP BY "u"."name", "id" ORDER BY "id"""" $ DLIQ.buildQuery q
35 | TM.result q [ { id: 1, "u.name": "josh" }, { id: 2, "u.name": "mary" } ]
36 |
--------------------------------------------------------------------------------
/test/Insert.purs:
--------------------------------------------------------------------------------
1 | module Test.Insert where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Data.Tuple.Nested ((/\))
8 | import Droplet.Language.Internal.Translate as DLIQ
9 | import Test.Model as TM
10 |
11 | import Test.Spec (Spec)
12 | import Test.Spec as TS
13 |
14 |
15 | tests ∷ Spec Unit
16 | tests = do
17 | TS.describe "insert" do
18 | TS.describe "values" do
19 | TS.it "all fields" do
20 | let q = insert # into users (name /\ surname /\ birthday /\ joined) # values ("mary" /\ "m." /\ TM.makeDate 2000 9 9 /\ TM.makeDate 2009 9 9)
21 | TM.parameterized """INSERT INTO "users"("name", "surname", "birthday", "joined") VALUES ($1, $2, $3, $4)""" $ DLIQ.buildQuery q
22 | TM.result' q []
23 | TS.it "some fields" do
24 | let q = insert # into tags name # values "my tag"
25 | TM.parameterized """INSERT INTO "tags"("name") VALUES ($1)""" $ DLIQ.buildQuery q
26 | TM.result' q []
27 | TS.describe "default values" do
28 | TS.it "single" do
29 | let q = insert # into users (name /\ surname /\ joined) # values ("josh" /\ "a" /\ Default)
30 | TM.parameterized """INSERT INTO "users"("name", "surname", "joined") VALUES ($1, $2, DEFAULT)""" $ DLIQ.buildQuery q
31 | TM.result' q []
32 | TS.it "all" do
33 | let q = insert # into doublePrimaryKey defaultValues
34 | TM.notParameterized """INSERT INTO "double_primary_key" DEFAULT VALUES""" $ DLIQ.buildQuery q
35 | TM.result' q []
36 | -- need some different design for this
37 | -- TS.it "many" do
38 | -- let q = insert # into users (name /\ surname /\ joined) # values ["josh" /\ "a" /\ TM.makeDate 2000 3 4, "josh" /\ "a" /\ Default]
39 | -- TM.parameterized """INSERT INTO "users"("name", "surname", "joined") VALUES ($1, $2, $3), ($4, $5, DEFAULT)""" $ DLIQ.buildQuery q
40 | -- TM.result' q []
41 | TS.describe "multiple" do
42 | TS.it "all fields" do
43 | let
44 | q = insert # into users (name /\ surname /\ birthday /\ joined) # values
45 | [ "mary" /\ "m." /\ TM.makeDate 2000 9 9 /\ TM.makeDate 2009 9 9
46 | , "john" /\ "j." /\ TM.makeDate 2000 9 9 /\ TM.makeDate 2009 9 9
47 | ]
48 | TM.parameterized """INSERT INTO "users"("name", "surname", "birthday", "joined") VALUES ($1, $2, $3, $4), ($5, $6, $7, $8)""" $ DLIQ.buildQuery q
49 | TM.result' q []
50 | TS.it "some fields" do
51 | let q = insert # into tags name # values [ "my tag", "my other tag" ]
52 | TM.parameterized """INSERT INTO "tags"("name") VALUES ($1), ($2)""" $ DLIQ.buildQuery q
53 | TM.result' q []
--------------------------------------------------------------------------------
/test/Join.purs:
--------------------------------------------------------------------------------
1 | module Test.Join where
2 |
3 | import Data.Maybe (Maybe(..))
4 | import Data.Tuple.Nested ((/\))
5 | import Droplet.Language (as, from, join, leftJoin, limit, on, orderBy, select, wher, (.&&.), (...), (.=.))
6 | import Droplet.Language.Internal.Translate as DLIQ
7 | import Prelude
8 | import Test.Model as TM
9 | import Test.Spec (Spec)
10 | import Type.Proxy(Proxy(..))
11 | import Test.Spec as TS
12 | import Test.Types (b, id, messages, n, name, sender, sent, t, tags, u, users)
13 |
14 | tests ∷ Spec Unit
15 | tests = do
16 | TS.describe "join" do
17 | TS.describe "inner" do
18 | TS.it "path column" do
19 | let q = select (u ... id /\ sender) # from ((users # as u) `join` (messages # as t) # on (u ... id .=. t ... id))
20 | TM.notParameterized """SELECT "u"."id" "u.id", "sender" FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
21 | TM.result q [ { "u.id": 1, sender: 1 }, { "u.id": 2, sender: 2 } ]
22 | TS.it "unique columns" do
23 | let q = select (sent /\ name) # from ((users # as u) `join` (messages # as t) # on (u ... id .=. t ... id))
24 | TM.notParameterized """SELECT "sent", "name" FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
25 | TM.result q [ { sent: true, name: "josh" }, { sent: true, name: "mary" } ]
26 | TS.it "aliased columns" do
27 | let q = select (u ... id # as id) # from ((users # as u) `join` (messages # as t) # on (u ... id .=. t ... id))
28 | TM.notParameterized """SELECT "u"."id" AS "id" FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
29 | TM.result q [ { id: 1 }, { id: 2 } ]
30 | TS.it "subquery with path column" do
31 | let q = select (select (u ... id) # from users # orderBy id # limit (Proxy :: _ 1)) # from ((users # as u) `join` (messages # as t) # on (u ... id .=. t ... id))
32 | TM.notParameterized """SELECT (SELECT "u"."id" "u.id" FROM "users" ORDER BY "id" LIMIT 1) FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
33 | TM.result q [ { "u.id": Just 1 }, { "u.id": Just 2 } ]
34 | TS.it "subquery with where path column" do
35 | let q = select (select id # from users # wher (id .=. u ... id) # orderBy id # limit (Proxy :: _ 1)) # from ((users # as u) `join` (messages # as t) # on (u ... id .=. t ... id))
36 | TM.notParameterized """SELECT (SELECT "id" FROM "users" WHERE "id" = "u"."id" ORDER BY "id" LIMIT 1) FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
37 | TM.result q [ { id: Just 1 }, { id: Just 2 } ]
38 | TS.it "aliased subquery with where path column" do
39 | let q = select (select id # from (users # as b) # wher (b ... id .=. u ... id) # orderBy id # limit (Proxy :: _ 1)) # from ((users # as u) `join` (messages # as t) # on (u ... id .=. t ... id))
40 | TM.notParameterized """SELECT (SELECT "id" FROM "users" AS "b" WHERE "b"."id" = "u"."id" ORDER BY "id" LIMIT 1) FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
41 | TM.result q [ { id: Just 1 }, { id: Just 2 } ]
42 | TS.it "three joins" do
43 | let q = select (u ... id /\ t ... id /\ b ... id) # from (((users # as u) `join` (messages # as t) # on (u ... id .=. t ... id)) `join` (tags # as b) # on (b ... id .=. u ... id))
44 | TM.notParameterized """SELECT "u"."id" "u.id", "t"."id" "t.id", "b"."id" "b.id" FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id" INNER JOIN "tags" AS "b" ON "b"."id" = "u"."id"""" $ DLIQ.buildQuery q
45 | TM.result q [ { "b.id": 1, "t.id": 1, "u.id": 1 } ]
46 | TS.it "four joins" do
47 | let q = select (u ... id /\ t ... id /\ b ... id /\ n ... id) # from ((((users # as u) `join` (messages # as t) # on (u ... id .=. t ... id)) `join` (tags # as b) # on (b ... id .=. u ... id)) `join` (users # as n) # on (n ... id .=. t ... id .&&. n ... id .=. u ... id))
48 | TM.notParameterized """SELECT "u"."id" "u.id", "t"."id" "t.id", "b"."id" "b.id", "n"."id" "n.id" FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "u"."id" = "t"."id" INNER JOIN "tags" AS "b" ON "b"."id" = "u"."id" INNER JOIN "users" AS "n" ON ("n"."id" = "t"."id" AND "n"."id" = "u"."id")""" $ DLIQ.buildQuery q
49 | TM.result q [ { "b.id": 1, "t.id": 1, "u.id": 1, "n.id": 1 } ]
50 | TS.it "subselect outer reference" do
51 | let q = select (select name # from (join (users # as n) (messages # as b) # on (b ... id .=. n ... id .&&. b ... id .=. u ... id)) # orderBy name # limit (Proxy :: _ 1)) # from (users # as u)
52 | TM.notParameterized """SELECT (SELECT "name" FROM "users" AS "n" INNER JOIN "messages" AS "b" ON ("b"."id" = "n"."id" AND "b"."id" = "u"."id") ORDER BY "name" LIMIT 1) FROM "users" AS "u"""" $ DLIQ.buildQuery q
53 | TM.result q [ { name: Just "josh" }, { name: Just "mary" } ]
54 |
55 | TS.describe "(left) outer" do
56 | TS.it "path column" do
57 | let q = select (u ... id /\ sender) # from ((users # as u) `leftJoin` (messages # as t) # on (u ... id .=. t ... id))
58 | TM.notParameterized """SELECT "u"."id" "u.id", "sender" FROM "users" AS "u" LEFT JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
59 | TM.result q [ { "u.id": 1, sender: Just 1 }, { "u.id": 2, sender: Just 2 } ]
60 | TS.it "unique columns" do
61 | let q = select (sent /\ name) # from ((users # as u) `leftJoin` (messages # as t) # on (u ... id .=. t ... id))
62 | TM.notParameterized """SELECT "sent", "name" FROM "users" AS "u" LEFT JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
63 | TM.result q [ { sent: Just true, name: "josh" }, { sent: Just true, name: "mary" } ]
64 | TS.it "aliased columns" do
65 | let q = select (u ... id # as id) # from ((users # as u) `leftJoin` (messages # as t) # on (u ... id .=. t ... id))
66 | TM.notParameterized """SELECT "u"."id" AS "id" FROM "users" AS "u" LEFT JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
67 | TM.result q [ { id: 1 }, { id: 2 } ]
68 | TS.it "subquery with path column" do
69 | let q = select (select (u ... id) # from users # orderBy id # limit (Proxy :: _ 1)) # from ((users # as u) `leftJoin` (messages # as t) # on (u ... id .=. t ... id))
70 | TM.notParameterized """SELECT (SELECT "u"."id" "u.id" FROM "users" ORDER BY "id" LIMIT 1) FROM "users" AS "u" LEFT JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
71 | TM.result q [ { "u.id": Just 1 }, { "u.id": Just 2 } ]
72 | TS.it "subquery with where path column" do
73 | let q = select (select id # from users # wher (id .=. u ... id) # orderBy id # limit (Proxy :: _ 1)) # from ((users # as u) `leftJoin` (messages # as t) # on (u ... id .=. t ... id))
74 | TM.notParameterized """SELECT (SELECT "id" FROM "users" WHERE "id" = "u"."id" ORDER BY "id" LIMIT 1) FROM "users" AS "u" LEFT JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
75 | TM.result q [ { id: Just 1 }, { id: Just 2 } ]
76 | TS.it "aliased subquery with where path column" do
77 | let q = select (select id # from (users # as b) # wher (b ... id .=. u ... id) # orderBy id # limit (Proxy :: _ 1)) # from ((users # as u) `leftJoin` (messages # as t) # on (u ... id .=. t ... id))
78 | TM.notParameterized """SELECT (SELECT "id" FROM "users" AS "b" WHERE "b"."id" = "u"."id" ORDER BY "id" LIMIT 1) FROM "users" AS "u" LEFT JOIN "messages" AS "t" ON "u"."id" = "t"."id"""" $ DLIQ.buildQuery q
79 | TM.result q [ { id: Just 1 }, { id: Just 2 } ]
80 | TS.it "three leftjoins" do
81 | let q = select (u ... id /\ t ... id /\ b ... id) # from (((users # as u) `leftJoin` (messages # as t) # on (u ... id .=. t ... id)) `leftJoin` (tags # as b) # on (b ... id .=. u ... id))
82 | TM.notParameterized """SELECT "u"."id" "u.id", "t"."id" "t.id", "b"."id" "b.id" FROM "users" AS "u" LEFT JOIN "messages" AS "t" ON "u"."id" = "t"."id" LEFT JOIN "tags" AS "b" ON "b"."id" = "u"."id"""" $ DLIQ.buildQuery q
83 | TM.result q [ { "b.id": Just 1, "t.id": Just 1, "u.id": 1 }, { "b.id": Nothing, "t.id": Just 2, "u.id": 2 } ]
84 | TS.it "four leftjoins" do
85 | let q = select (u ... id /\ t ... id /\ b ... id /\ n ... id) # from ((((users # as u) `leftJoin` (messages # as t) # on (u ... id .=. t ... id)) `leftJoin` (tags # as b) # on (b ... id .=. u ... id)) `leftJoin` (users # as n) # on (n ... id .=. t ... id .&&. n ... id .=. u ... id))
86 | TM.notParameterized """SELECT "u"."id" "u.id", "t"."id" "t.id", "b"."id" "b.id", "n"."id" "n.id" FROM "users" AS "u" LEFT JOIN "messages" AS "t" ON "u"."id" = "t"."id" LEFT JOIN "tags" AS "b" ON "b"."id" = "u"."id" LEFT JOIN "users" AS "n" ON ("n"."id" = "t"."id" AND "n"."id" = "u"."id")""" $ DLIQ.buildQuery q
87 | TM.result q [ { "b.id": Just 1, "t.id": Just 1, "u.id": 1, "n.id": Just 1 }, { "b.id": Nothing, "t.id": Just 2, "u.id": 2, "n.id": Just 2 } ]
88 | TS.it "subselect outer reference" do
89 | let q = select (select name # from (leftJoin (users # as n) (messages # as b) # on (b ... id .=. n ... id .&&. b ... id .=. u ... id)) # orderBy name # limit (Proxy :: _ 1)) # from (users # as u)
90 | TM.notParameterized """SELECT (SELECT "name" FROM "users" AS "n" LEFT JOIN "messages" AS "b" ON ("b"."id" = "n"."id" AND "b"."id" = "u"."id") ORDER BY "name" LIMIT 1) FROM "users" AS "u"""" $ DLIQ.buildQuery q
91 | TM.result q [ { name: Just "josh" }, { name: Just "josh" } ]
92 |
--------------------------------------------------------------------------------
/test/Limit.purs:
--------------------------------------------------------------------------------
1 | module Test.Limit where
2 |
3 | import Droplet.Language (as, from, limit, offset, orderBy, select, wher, (.<>.))
4 | import Prelude
5 | import Test.Types (id, n, name, users)
6 |
7 | import Data.Tuple.Nested ((/\))
8 | import Droplet.Language.Internal.Translate as DLIQ
9 | import Test.Model as TM
10 |
11 | import Test.Spec (Spec)
12 | import Test.Spec as TS
13 | import Type.Proxy(Proxy(..))
14 |
15 | tests ∷ Spec Unit
16 | tests =
17 | TS.describe "limit" do
18 | TS.it "from" do
19 | let q = select (4 # as n) # from users # orderBy n # limit (Proxy :: _ 4)
20 | TM.notParameterized """SELECT 4 AS "n" FROM "users" ORDER BY "n" LIMIT 4""" $ DLIQ.buildQuery q
21 | TM.result q [ { n: 4 }, { n: 4 } ]
22 | TS.it "where" do
23 | let q = select id # from users # wher (id .<>. 10) # orderBy (id /\ name) # limit (Proxy :: _ 2)
24 | TM.parameterized """SELECT "id" FROM "users" WHERE "id" <> $1 ORDER BY "id", "name" LIMIT 2""" $ DLIQ.buildQuery q
25 | TM.result q [ { id: 1 }, { id: 2 } ]
26 | TS.it "offset" do
27 | let q = select (4 # as n) # from users # orderBy n # offset 4 # limit (Proxy :: _ 5)
28 | TM.notParameterized """SELECT 4 AS "n" FROM "users" ORDER BY "n" OFFSET 4 LIMIT 5""" $ DLIQ.buildQuery q
29 | TM.result q []
30 |
--------------------------------------------------------------------------------
/test/Main.purs:
--------------------------------------------------------------------------------
1 | module Test.Main where
2 |
3 | import Prelude
4 |
5 | import Effect (Effect)
6 | import Effect.Aff as EA
7 | import Test.As as TA
8 | import Test.Create as TC
9 | import Test.Delete as TD
10 | import Test.Distinct as TDS
11 | import Test.From as TF
12 | import Test.Function as TFC
13 | import Test.GroupBy as TG
14 | import Test.Insert as TI
15 | import Test.Join as TJ
16 | import Test.Limit as TL
17 | import Test.NakedSelect as TNS
18 | import Test.Migration as TMG
19 | import Test.Offset as TOF
20 | import Test.OrderBy as TO
21 | import Test.Returning as TR
22 | import Test.Spec.Runner as TSR
23 | import Test.Spec.Reporter.Console (consoleReporter)
24 | import Test.SubQuery as TSQ
25 | import Test.Transaction as TST
26 | import Test.Union as TUN
27 | import Test.Unsafe as TUS
28 | import Test.Update as TU
29 | import Test.Drop as TDR
30 | import Test.Alter as TAL
31 | import Test.Where as TW
32 |
33 | main ∷ Effect Unit
34 | main = EA.launchAff_ $ TSR.runSpec [consoleReporter] do
35 | TUS.tests
36 | TI.tests
37 | TU.tests
38 | TD.tests
39 | TR.tests
40 | TF.tests
41 | TDS.tests
42 | TJ.tests
43 | TAL.tests
44 | TW.tests
45 | TUN.tests
46 | TG.tests
47 | TA.tests
48 | TSQ.tests
49 | TMG.tests
50 | TOF.tests
51 | TDR.tests
52 | TO.tests
53 | TFC.tests
54 | TL.tests
55 | TNS.tests
56 | TST.tests
57 | TC.tests
--------------------------------------------------------------------------------
/test/Migration.purs:
--------------------------------------------------------------------------------
1 | module Test.Migration where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Control.Monad.Error.Class as CMEC
8 | import Control.Monad.Error.Class as EA
9 | import Data.Maybe (Maybe(..))
10 | import Droplet.Driver as DD
11 | import Droplet.Driver.Internal.Migration as DDIM
12 | import Droplet.Driver.Internal.Query as DDIQ
13 | import Droplet.Driver.Migration as DDM
14 | import Effect.Class (liftEffect)
15 | import Effect.Exception as EE
16 | import Test.Model (connectionInfo)
17 | import Test.Model as TM
18 | import Test.Spec (Spec)
19 | import Test.Spec as TS
20 |
21 | tests ∷ Spec Unit
22 | tests = TS.describe "migration" do
23 | TS.it "runs all migrations" do
24 | pool ← liftEffect $ DD.newPool connectionInfo
25 | DDM.migrate pool
26 | [ { up: \c → void <<< DD.execute c $ create # table migrated, down: const (pure unit), identifier: "aaa" }
27 | , { up: \c → void $ DDIQ.execute c $ insert # into migrated id # values 3, down: const (pure unit), identifier: "bbb" }
28 | ]
29 | let q = select id # from migrated
30 | TM.result q [ { id: 3 } ]
31 | TS.it "doesn't fail if table already exists" do
32 | pool ← liftEffect $ DD.newPool connectionInfo
33 | void $ DD.withTransaction pool $ \c → DDIM.createMigrationTable c
34 | DDM.migrate pool []
35 | TS.it "skip migrations already run" do
36 | pool ← liftEffect $ DD.newPool connectionInfo
37 | -- mark step as run
38 | void $ DD.withTransaction pool $ \c → do
39 | DDIM.createMigrationTable c
40 | DDIM.markAsRun c { identifier: "bbb" }
41 | DDM.migrate pool
42 | [ { up: \c → void <<< DD.execute c $ create # table migrated, down: const (pure unit), identifier: "aaa" }
43 | , { up: \c → void $ DDIQ.execute c $ insert # into migrated id # values 3, down: const (pure unit), identifier: "bbb" }
44 | ]
45 | let q = select id # from migrated
46 | TM.result q []
47 | TS.it "all or nothing" do
48 | pool ← liftEffect $ DD.newPool connectionInfo
49 | void $ DD.withTransaction pool $ \c → DD.execute c $ create # table migrated
50 | void $ CMEC.try $ DDM.migrate pool
51 | [ { up: \c → void $ DDIQ.execute c $ insert # into migrated id # values 3, down: const (pure unit), identifier: "bbb" }
52 | , { up: const (EA.throwError $ EE.error "error"), down: const (pure unit), identifier: "ccc" }
53 | ]
54 | let q = select id # from migrated
55 | TM.result q []
--------------------------------------------------------------------------------
/test/Model.purs:
--------------------------------------------------------------------------------
1 | module Test.Model where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Data.Date (Date)
8 | import Data.Date (canonicalDate) as DD
9 | import Data.DateTime (DateTime(..), Time(..))
10 | import Data.Either (Either(..))
11 | import Data.Enum (class BoundedEnum)
12 | import Data.Enum as DE
13 | import Data.Eq (class EqRecord)
14 | import Data.Maybe (Maybe(..))
15 | import Data.Maybe as DM
16 | import Data.Show (class ShowRecordFields)
17 | import Data.Tuple.Nested ((/\))
18 | import Droplet.Driver (class FromResult, Configuration, Connection, PgError)
19 | import Droplet.Driver (defaultConfiguration, newPool, query, withConnection) as DD
20 | import Droplet.Driver.Unsafe as DDU
21 | import Droplet.Language.Internal.Translate (class ToQuery, Query(..))
22 | import Effect.Aff (Aff)
23 | import Effect.Class (liftEffect)
24 | import Partial.Unsafe as PU
25 | import Prim.Row (class Nub)
26 | import Prim.RowList (class RowToList)
27 | import Test.Spec (Spec)
28 | import Test.Spec as TS
29 | import Test.Spec.Assertions as TSA
30 |
31 | makeDate ∷ Int → Int → Int → Date
32 | makeDate y m d = DD.canonicalDate (unsafeToEnum y) (unsafeToEnum m) (unsafeToEnum d)
33 |
34 | makeDateTime ∷ Int → Int → Int → DateTime
35 | makeDateTime y m d = DateTime (makeDate y m d) $ Time (unsafeToEnum 0) (unsafeToEnum 0) (unsafeToEnum 0) (unsafeToEnum 0)
36 |
37 | unsafeToEnum ∷ ∀ a. BoundedEnum a ⇒ Int → a
38 | unsafeToEnum v = PU.unsafePartial (DM.fromJust $ DE.toEnum v)
39 |
40 | notParameterized s (Query _ q parameters) = case parameters of
41 | [] → TSA.shouldEqual s q
42 | _ → TSA.fail $ "Expected no parameters for " <> s
43 |
44 | parameterized s (Query _ q parameters) = case parameters of
45 | [] → TSA.fail $ "Expected parameters for " <> s
46 | _ → TSA.shouldEqual s q
47 |
48 | -- | Compares query return to expected result
49 | -- |
50 | -- | Default records are inserted before running the query. Use result' if that's not the intended behavior
51 | result ∷ ∀ t51 t52 t53. Nub t52 t52 => ToQuery t51 t52 ⇒ RowToList t52 t53 ⇒ FromResult t53 (Record t52) ⇒ EqRecord t53 t52 ⇒ ShowRecordFields t53 t52 ⇒ t51 → Array (Record t52) → Aff Unit
52 | result q o = do
53 | pool ← liftEffect $ DD.newPool connectionInfo
54 | DD.withConnection pool case _ of
55 | Left error → TSA.fail $ "Error connecting" <> show error
56 | Right connection → do
57 | insertDefaultRecords
58 | r ← DD.query connection q
59 | truncateTables connection
60 | TSA.shouldEqual (Right o) r
61 |
62 | result' ∷ ∀ t51 t52 t53. Nub t52 t52 => ToQuery t51 t52 ⇒ RowToList t52 t53 ⇒ FromResult t53 (Record t52) ⇒ EqRecord t53 t52 ⇒ ShowRecordFields t53 t52 ⇒ t51 → Array (Record t52) → Aff Unit
63 | result' q o = do
64 | pool ← liftEffect $ DD.newPool connectionInfo
65 | DD.withConnection pool case _ of
66 | Left error → TSA.fail $ "Error connecting" <> show error
67 | Right connection → do
68 | r ← DD.query connection q
69 | truncateTables connection
70 | TSA.shouldEqual (Right o) r
71 |
72 | resultOnly ∷ ∀ t51 t52 t53. ToQuery t51 t52 ⇒ RowToList t52 t53 ⇒ FromResult t53 (Record t52) ⇒ EqRecord t53 t52 ⇒ ShowRecordFields t53 t52 ⇒ t51 → Aff (Array (Record t52))
73 | resultOnly q = do
74 | pool ← liftEffect $ DD.newPool connectionInfo
75 | DD.withConnection pool case _ of
76 | Left error → do
77 | TSA.fail $ "Error connecting" <> show error
78 | pure []
79 | Right connection → do
80 | insertDefaultRecords
81 | r ← DD.query connection q
82 | truncateTables connection
83 | case r of
84 | Right o → pure o
85 | Left e → do
86 | TSA.fail $ "Error running query: " <> show e
87 | pure []
88 |
89 | unsafeResult ∷ ∀ re parameters par result. Nub result result => RowToList result re ⇒ RowToList parameters par ⇒ ToParameters parameters par ⇒ FromResult re (Record result) ⇒ EqRecord re result ⇒ ShowRecordFields re result ⇒ Maybe Plan → String → Record parameters → Array (Record result) → Aff Unit
90 | unsafeResult plan q parameters o = do
91 | pool ← liftEffect $ DD.newPool connectionInfo
92 | DD.withConnection pool case _ of
93 | Left error → TSA.fail $ "Error connecting" <> show error
94 | Right connection → do
95 | insertDefaultRecords
96 | r ← DDU.unsafeQuery connection plan q parameters
97 | truncateTables connection
98 | TSA.shouldEqual (Right o) r
99 |
100 | truncateTables ∷ Connection → Aff Unit
101 | truncateTables connection = void (DDU.unsafeQuery connection Nothing "select truncate_tables()" {} ∷ Aff (Either PgError (Array {})))
102 |
103 | insertDefaultRecords ∷ Aff Unit
104 | insertDefaultRecords = do
105 | pool ← liftEffect $ DD.newPool connectionInfo
106 | DD.withConnection pool case _ of
107 | Left error → TSA.fail $ "Error connecting" <> show error
108 | Right connection → do
109 | void <<< DD.query connection $ insert # into users (name /\ surname /\ birthday) # values ("josh" /\ "j." /\ makeDate 1990 1 1)
110 | void <<< DD.query connection $ insert # into users (name /\ surname /\ birthday) # values ("mary" /\ "sue" /\ makeDate 1900 11 11)
111 | void <<< DD.query connection $ insert # into messages (sender /\ recipient /\ sent /\ date /\ secondDate) # values (1 /\ 2 /\ true /\ makeDateTime 2000 3 4 /\ makeDateTime 2000 3 4)
112 | void <<< DD.query connection $ insert # into messages (sender /\ recipient /\ sent /\ date /\ secondDate) # values (2 /\ 1 /\ true /\ makeDateTime 2000 3 4 /\ makeDateTime 2000 3 4)
113 | void <<< DD.query connection $ insert # into tags (name /\ _by) # values ("tagged" /\ Just 1)
114 | void <<< DD.query connection $ insert # into maybeKeys id # values 1
115 | void <<< DD.query connection $ insert # into uniqueValues (name /\ _by) # values ("named" /\ Just 1)
116 | void <<< DD.query connection $ insert # into defaultColumns (recipient /\ sender) # values (RecipientColumn 3 /\ SenderColumn 1)
117 | void <<< DD.query connection $ insert # into doublePrimaryKey defaultValues
118 | void <<< DD.query connection $ insert # into composite (secondId /\ name /\ sender /\ recipient) # values ( 1 /\ "adam" /\ 1 /\ 2)
119 |
120 | connectionInfo ∷ Configuration
121 | connectionInfo = (DD.defaultConfiguration "droplet")
122 | { user = Just "droplet"
123 | , host = Nothing
124 | , password = Just "droplet"
125 | , idleTimeoutMillis = Just 1000
126 | }
--------------------------------------------------------------------------------
/test/NakedSelect.purs:
--------------------------------------------------------------------------------
1 | module Test.NakedSelect where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Data.Maybe (Maybe(..))
8 | import Data.Tuple.Nested ((/\))
9 | import Droplet.Language.Internal.Translate as DLIQ
10 | import Test.Model as TM
11 | import Type.Proxy(Proxy(..))
12 | import Test.Spec (Spec)
13 | import Test.Spec as TS
14 |
15 |
16 | tests ∷ Spec Unit
17 | tests =
18 | TS.describe "naked select" do
19 | TS.it "scalar" do
20 | let q = select (3 # as n)
21 | TM.notParameterized """SELECT 3 AS "n"""" $ DLIQ.buildQuery q
22 | TM.result q [ { n: 3 } ]
23 | TS.describe "function" do
24 | TS.it "regular" do
25 | let q = select (date_part_age ("year" /\ TM.makeDateTime 2000 1 1) # as u)
26 | TM.parameterized """SELECT date_part_age($1, $2) AS "u"""" $ DLIQ.buildQuery q
27 | void $ TM.resultOnly q
28 | TS.it "side effect" do
29 | let q = select (fire_missiles (9 /\ 8) # as u)
30 | TM.parameterized """SELECT fire_missiles($1, $2) AS "u"""" $ DLIQ.buildQuery q
31 | TM.result q [ { u: unit } ]
32 | TS.it "side effect without parameters" do
33 | let q = select (random # as u)
34 | TM.notParameterized """SELECT random() AS "u"""" $ DLIQ.buildQuery q
35 | void $ TM.resultOnly q
36 | TS.it "subquery" do
37 | let q = select (select (34 # as n) # from users # wher (name .=. name) # orderBy id # limit (Proxy :: _ 1))
38 | TM.notParameterized """SELECT (SELECT 34 AS "n" FROM "users" WHERE "name" = "name" ORDER BY "id" LIMIT 1)""" $ DLIQ.buildQuery q
39 | TM.result q [ { n: Just 34 } ]
40 | TS.it "aliases" do
41 | let q = select (select (u ... id) # from (users # as u) # orderBy id # limit (Proxy :: _ 1))
42 | TM.notParameterized """SELECT (SELECT "u"."id" "u.id" FROM "users" AS "u" ORDER BY "id" LIMIT 1)""" $ DLIQ.buildQuery q
43 | TM.result q [ { "u.id": Just 1 } ]
44 | TS.it "named subquery" do
45 | let q = select (select (34 # as n) # from users # wher (name .=. name) # orderBy id # limit (Proxy :: _ 1) # as t)
46 | TM.notParameterized """SELECT (SELECT 34 AS "n" FROM "users" WHERE "name" = "name" ORDER BY "id" LIMIT 1) AS "t"""" $ DLIQ.buildQuery q
47 | TM.result q [ { t: Just 34 } ]
48 | TS.it "tuple" do
49 | let q = select ((3 # as b) /\ (select name # from users # orderBy name # limit (Proxy :: _ 1)) /\ (select (u ... name # as n) # from (users # as u) # orderBy (name # desc) # limit (Proxy :: _ 1)))
50 | TM.notParameterized """SELECT 3 AS "b", (SELECT "name" FROM "users" ORDER BY "name" LIMIT 1), (SELECT "u"."name" AS "n" FROM "users" AS "u" ORDER BY "name" DESC LIMIT 1)""" $ DLIQ.buildQuery q
51 | TM.result q [ { b: 3, name: Just "josh", n: Just "mary" } ]
52 |
--------------------------------------------------------------------------------
/test/Offset.purs:
--------------------------------------------------------------------------------
1 | module Test.Offset where
2 |
3 | import Droplet.Language (as, from, limit, offset, orderBy, select, wher, (.<>.))
4 | import Prelude
5 | import Test.Types (id, n, name, users)
6 |
7 | import Data.Tuple.Nested ((/\))
8 | import Droplet.Language.Internal.Translate as DLIQ
9 | import Test.Model as TM
10 |
11 | import Test.Spec (Spec)
12 | import Test.Spec as TS
13 | import Type.Proxy(Proxy(..))
14 |
15 | tests ∷ Spec Unit
16 | tests =
17 | TS.describe "offset" do
18 | TS.it "from" do
19 | let q = select (4 # as n) # from users # orderBy n # offset 1
20 | TM.notParameterized """SELECT 4 AS "n" FROM "users" ORDER BY "n" OFFSET 1""" $ DLIQ.buildQuery q
21 | TM.result q [ { n: 4 } ]
22 | TS.it "where" do
23 | let q = select id # from users # wher (id .<>. 10) # orderBy (id /\ name) # offset 2
24 | TM.parameterized """SELECT "id" FROM "users" WHERE "id" <> $1 ORDER BY "id", "name" OFFSET 2""" $ DLIQ.buildQuery q
25 | TM.result q []
26 | TS.it "limit" do
27 | let q = select (4 # as n) # from users # orderBy n # limit (Proxy :: _ 5) # offset 4
28 | TM.notParameterized """SELECT 4 AS "n" FROM "users" ORDER BY "n" LIMIT 5 OFFSET 4""" $ DLIQ.buildQuery q
29 | TM.result q []
--------------------------------------------------------------------------------
/test/OrderBy.purs:
--------------------------------------------------------------------------------
1 | module Test.OrderBy where
2 |
3 | import Data.Tuple.Nested ((/\))
4 | import Droplet.Language
5 | import Droplet.Language.Internal.Translate as DLIQ
6 | import Prelude hiding (join)
7 | import Test.Model as TM
8 | import Test.Types (date_part_age, id, date, messages, n, name, t, u, users)
9 |
10 | import Test.Spec (Spec)
11 | import Test.Spec as TS
12 |
13 |
14 | tests ∷ Spec Unit
15 | tests =
16 | TS.describe "order by" do
17 | TS.it "projection" do
18 | let q = select (4 # as n) # from users # orderBy n
19 | TM.notParameterized """SELECT 4 AS "n" FROM "users" ORDER BY "n"""" $ DLIQ.buildQuery q
20 | TM.result q [ { n: 4 }, { n: 4 } ]
21 | TS.it "field name" do
22 | let q = select id # from users # wher (id .<>. 10) # orderBy (id /\ name)
23 | TM.parameterized """SELECT "id" FROM "users" WHERE "id" <> $1 ORDER BY "id", "name"""" $ DLIQ.buildQuery q
24 | TM.result q [ { id: 1 }, { id: 2 } ]
25 | TS.it "asc" do
26 | let q = select (id # as n) # from users # wher (id .<>. 4) # orderBy (n /\ (name # asc))
27 | TM.parameterized """SELECT "id" AS "n" FROM "users" WHERE "id" <> $1 ORDER BY "n", "name" ASC""" $ DLIQ.buildQuery q
28 | TM.result q [ { n: 1 }, { n: 2 } ]
29 | TS.it "desc" do
30 | let q = select id # from users # orderBy (id # desc)
31 | TM.notParameterized """SELECT "id" FROM "users" ORDER BY "id" DESC""" $ DLIQ.buildQuery q
32 | TM.result q [ { id: 2 }, { id: 1 } ]
33 | TS.it "distinct" do
34 | let q = select (distinct (id # as n)) # from users # orderBy n
35 | TM.notParameterized """SELECT DISTINCT "id" AS "n" FROM "users" ORDER BY "n"""" $ DLIQ.buildQuery q
36 | TM.result q [ { n: 1 }, { n: 2 } ]
37 | TS.describe "function" do
38 | TS.it "regular" do
39 | let q = select (4 # as n) # from users # orderBy (date_part_age ("year" /\ TM.makeDateTime 2000 1 1))
40 | TM.parameterized """SELECT 4 AS "n" FROM "users" ORDER BY date_part_age($1, $2)""" $ DLIQ.buildQuery q
41 | TM.result q [ { n: 4 }, { n: 4 } ]
42 | TS.it "no parameters" do
43 | let q = select (4 # as n) # from users # orderBy random
44 | TM.notParameterized """SELECT 4 AS "n" FROM "users" ORDER BY random()""" $ DLIQ.buildQuery q
45 | void $ TM.resultOnly q
46 | TS.describe "path" do
47 | TS.it "field name" do
48 | let q = select id # from (users # as u) # orderBy (u ... id)
49 | TM.notParameterized """SELECT "id" FROM "users" AS "u" ORDER BY "u"."id"""" $ DLIQ.buildQuery q
50 | TM.result q [ { id: 1 }, { id: 2 } ]
51 | TS.it "asc" do
52 | let q = select id # from (select id # from users # as u) # orderBy (u ... id # asc)
53 | TM.notParameterized """SELECT "id" FROM (SELECT "id" FROM "users") AS "u" ORDER BY "u"."id" ASC""" $ DLIQ.buildQuery q
54 | TM.result q [ { id: 1 }, { id: 2 } ]
55 | TS.it "desc" do
56 | let q = select (3 # as id) # from (join (users # as u) (messages # as t) # on (t ... id .=. u ... id)) # orderBy (u ... id # desc)
57 | TM.notParameterized """SELECT 3 AS "id" FROM "users" AS "u" INNER JOIN "messages" AS "t" ON "t"."id" = "u"."id" ORDER BY "u"."id" DESC""" $ DLIQ.buildQuery q
58 | TM.result q [ { id: 3 }, { id: 3 } ]
59 | TS.it "function" do
60 | let q = select (4 # as n) # from (messages # as u) # orderBy (date_part_age ("year" /\ u ... date))
61 | TM.parameterized """SELECT 4 AS "n" FROM "messages" AS "u" ORDER BY date_part_age($1, "u"."date")""" $ DLIQ.buildQuery q
62 | TM.result q [ { n: 4 }, { n: 4 } ]
63 |
--------------------------------------------------------------------------------
/test/Returning.purs:
--------------------------------------------------------------------------------
1 | module Test.Returning where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Data.Tuple.Nested ((/\))
8 | import Droplet.Language.Internal.Translate as DLIQ
9 | import Test.Model as TM
10 |
11 | import Test.Spec (Spec)
12 | import Test.Spec as TS
13 |
14 |
15 | tests ∷ Spec Unit
16 | tests = do
17 | TS.describe "returning" do
18 | TS.it "default values" do
19 | let q = insert # into doublePrimaryKey defaultValues # returning id
20 | TM.notParameterized """INSERT INTO "double_primary_key" DEFAULT VALUES RETURNING "id"""" $ DLIQ.buildQuery q
21 | TM.result' q [ { id: 1 } ]
22 | TS.it "single field" do
23 | let q = insert # into users (name /\ surname /\ birthday /\ joined) # values ("mary" /\ "m." /\ TM.makeDate 2000 9 9 /\ TM.makeDate 2009 9 9) # returning id
24 | TM.parameterized """INSERT INTO "users"("name", "surname", "birthday", "joined") VALUES ($1, $2, $3, $4) RETURNING "id"""" $ DLIQ.buildQuery q
25 | TM.result' q [ { id: 1 } ]
26 | TS.it "tuple" do
27 | let q = insert # into users (name /\ surname /\ birthday /\ joined) # values ("mary" /\ "m." /\ TM.makeDate 2000 9 9 /\ TM.makeDate 2009 9 9) # returning (id /\ name)
28 | TM.parameterized """INSERT INTO "users"("name", "surname", "birthday", "joined") VALUES ($1, $2, $3, $4) RETURNING "id", "name"""" $ DLIQ.buildQuery q
29 | TM.result' q [ { id: 1, name: "mary" } ]
--------------------------------------------------------------------------------
/test/SubQuery.purs:
--------------------------------------------------------------------------------
1 | module Test.SubQuery where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Data.Maybe (Maybe(..))
8 | import Droplet.Language.Internal.Translate as DLIQ
9 | import Test.Model as TM
10 | import Type.Proxy(Proxy(..))
11 | import Test.Spec (Spec)
12 | import Test.Spec as TS
13 |
14 |
15 | tests ∷ Spec Unit
16 | tests = do
17 | TS.describe "subquery" do
18 | TS.it "null" do
19 | let q = select (select id # from users # wher (id .=. 9999) # orderBy id # limit (Proxy :: _ 1)) # from messages # orderBy id # limit (Proxy :: _ 1)
20 | TM.parameterized """SELECT (SELECT "id" FROM "users" WHERE "id" = $1 ORDER BY "id" LIMIT 1) FROM "messages" ORDER BY "id" LIMIT 1""" $ DLIQ.buildQuery q
21 | TM.result q [ { id: Nothing } ]
22 | TS.it "nested null" do
23 | let q = select (select created # from tags # orderBy id # limit (Proxy :: _ 1)) # from messages # orderBy id # limit (Proxy :: _ 1)
24 | TM.notParameterized """SELECT (SELECT "created" FROM "tags" ORDER BY "id" LIMIT 1) FROM "messages" ORDER BY "id" LIMIT 1""" $ DLIQ.buildQuery q
25 | --avoid (Maybe (Maybe t))
26 | TM.result q [ { created: Nothing } ]
27 | TS.it "function" do
28 | let q = select (select (coalesce id # as id) # from users # orderBy id # limit (Proxy :: _ 1)) # from messages
29 | TM.notParameterized """SELECT (SELECT coalesce("id") AS "id" FROM "users" ORDER BY "id" LIMIT 1) FROM "messages"""" $ DLIQ.buildQuery q
30 | TM.result q [ { id: Just 1 }, { id: Just 1 } ]
31 | TS.describe "outer references" do
32 | TS.describe "projection from table" do
33 | TS.it "field" do
34 | let q = select (select (u ... id) # from users # orderBy id # limit (Proxy :: _ 1)) # from (users # as u) # orderBy id # limit (Proxy :: _ 1)
35 | TM.notParameterized """SELECT (SELECT "u"."id" "u.id" FROM "users" ORDER BY "id" LIMIT 1) FROM "users" AS "u" ORDER BY "id" LIMIT 1""" $ DLIQ.buildQuery q
36 | TM.result q [ { "u.id": Just 1 } ]
37 | TS.it "alias" do
38 | let q = select (select (u ... id # as n) # from users # orderBy id # limit (Proxy :: _ 1)) # from (users # as u) # orderBy id # limit (Proxy :: _ 1)
39 | TM.notParameterized """SELECT (SELECT "u"."id" AS "n" FROM "users" ORDER BY "id" LIMIT 1) FROM "users" AS "u" ORDER BY "id" LIMIT 1""" $ DLIQ.buildQuery q
40 | TM.result q [ { n: Just 1 } ]
41 | TS.it "same table different alias" do
42 | let q = select (select (n ... name) # from (users # as n) # orderBy id # limit (Proxy :: _ 1)) # from (users # as u)
43 | TM.notParameterized """SELECT (SELECT "n"."name" "n.name" FROM "users" AS "n" ORDER BY "id" LIMIT 1) FROM "users" AS "u"""" $ DLIQ.buildQuery q
44 | TM.result q [ { "n.name": Just "josh" }, { "n.name": Just "josh" } ]
45 | TS.it "same table alias" do
46 | let q = select (select (u ... sent) # from (messages # as u) # orderBy id # limit (Proxy :: _ 1)) # from (users # as u)
47 | TM.notParameterized """SELECT (SELECT "u"."sent" "u.sent" FROM "messages" AS "u" ORDER BY "id" LIMIT 1) FROM "users" AS "u"""" $ DLIQ.buildQuery q
48 | TM.result q [ { "u.sent": Just true }, { "u.sent": Just true } ]
49 | TS.describe "projection from named query" do
50 | TS.it "field" do
51 | let q = select (select (u ... id) # from users # orderBy id # limit (Proxy :: _ 1)) # from (select id # from users # as u)
52 | TM.notParameterized """SELECT (SELECT "u"."id" "u.id" FROM "users" ORDER BY "id" LIMIT 1) FROM (SELECT "id" FROM "users") AS "u"""" $ DLIQ.buildQuery q
53 | TM.result q [ { "u.id": Just 1 }, { "u.id": Just 2 } ]
54 | TS.it "alias" do
55 | let q = select (select (u ... id # as n) # from users # orderBy id # limit (Proxy :: _ 1)) # from (select id # from users # as u)
56 | TM.notParameterized """SELECT (SELECT "u"."id" AS "n" FROM "users" ORDER BY "id" LIMIT 1) FROM (SELECT "id" FROM "users") AS "u"""" $ DLIQ.buildQuery q
57 | TM.result q [ { n: Just 1 }, { n: Just 2 } ]
--------------------------------------------------------------------------------
/test/Transaction.purs:
--------------------------------------------------------------------------------
1 | module Test.Transaction where
2 |
3 | import Droplet.Language
4 | import Prelude
5 | import Test.Types
6 |
7 | import Control.Monad.Error.Class as EA
8 | import Data.Either (Either(..))
9 | import Data.Maybe (Maybe(..))
10 | import Data.Tuple.Nested ((/\))
11 | import Droplet.Driver as DD
12 | import Effect.Class (liftEffect)
13 | import Effect.Exception as EE
14 | import Test.Model (connectionInfo)
15 | import Test.Model as TM
16 |
17 | import Test.Spec (Spec)
18 | import Test.Spec as TS
19 | import Test.Spec.Assertions as TSA
20 |
21 | tests ∷ Spec Unit
22 | tests = do
23 | TS.describe "transactions" do
24 | TS.it "commited" do
25 | pool ← liftEffect $ DD.newPool connectionInfo
26 | void $ DD.withTransaction pool $ \connection → do
27 | TM.truncateTables connection
28 |
29 | let ins = insert # into users (name /\ surname /\ birthday) # values ("josh" /\ "j." /\ TM.makeDate 1990 1 1)
30 | errors ← DD.execute connection ins
31 | TSA.shouldEqual Nothing errors
32 |
33 | let sel = select id # from users
34 | result ← DD.single connection sel
35 | TSA.shouldEqual (Right (Just { id: 1 })) result
36 |
37 | let upd = update users # set ((name .=. "Mary") /\ (surname .=. "Sue")) # wher (id .=. 1)
38 | errors ← DD.execute connection upd
39 | TSA.shouldEqual Nothing errors
40 |
41 | let q = select (name /\ surname) # from users
42 | TM.result' q [ { name: "Mary", surname: "Sue" } ]
43 | TS.it "rolled back" do
44 | pool ← liftEffect $ DD.newPool connectionInfo
45 | flip EA.catchError (const (pure unit)) <<< void <<< DD.withTransaction pool $ \connection → do
46 | TM.truncateTables connection
47 |
48 | let ins = insert # into users (name /\ surname /\ birthday) # values ("josh" /\ "j." /\ TM.makeDate 1990 1 1)
49 | errors ← DD.execute connection ins
50 | TSA.shouldEqual Nothing errors
51 |
52 | void <<< EA.throwError $ EE.error "pretend it happened for some reason"
53 |
54 | let upd = update users # set ((name .=. "Mary") /\ (surname .=. "Sue")) # wher (id .=. 1)
55 | errors2 ← DD.execute connection upd
56 | TSA.shouldEqual Nothing errors2
57 |
58 | let q = select (name /\ surname) # from users
59 | TM.result' q []
60 |
--------------------------------------------------------------------------------
/test/Types.purs:
--------------------------------------------------------------------------------
1 | module Test.Types where
2 |
3 | import Prelude
4 |
5 | import Prim hiding (Constraint)
6 | import Data.Date (Date)
7 | import Data.DateTime (DateTime)
8 | import Data.Maybe (Maybe)
9 | import Data.Tuple.Nested (type (/\))
10 | import Droplet.Language
11 | import Type.Proxy (Proxy(..))
12 |
13 | type Users =
14 | ( id ∷ Column Int (PrimaryKey /\ Identity)
15 | , name ∷ String
16 | , surname ∷ String
17 | , birthday ∷ Column Date Default
18 | , joined ∷ Column Date Default
19 | )
20 |
21 | type UsersTable = Table "users" Users
22 |
23 | type Messages =
24 | ( id ∷ Column Int (PrimaryKey /\ Identity)
25 | , sender ∷ Column Int (Constraint "sender_user" (ForeignKey "id" UsersTable))
26 | , recipient ∷ Int
27 | , date ∷ Column DateTime Default
28 | , second_date ∷ Column DateTime Default
29 | , sent ∷ Boolean
30 | )
31 |
32 | type Tags =
33 | ( id ∷ Column Int (PrimaryKey /\ Identity)
34 | , name ∷ String
35 | , created ∷ Maybe Date
36 | , by ∷ Column (Maybe Int) (Constraint "by_user" (ForeignKey "id" UsersTable))
37 | )
38 |
39 | type MaybeKeys =
40 | ( id ∷ Column Int PrimaryKey
41 | )
42 |
43 | type UniqueValues =
44 | ( name ∷ Column String Unique
45 | , by ∷ Column (Maybe Int) Unique
46 | )
47 |
48 | type DefaultColumns =
49 | ( sender ∷ Column SenderColumn (Unique /\ Default)
50 | , recipient ∷ Column RecipientColumn Default
51 | )
52 |
53 | type DoublePrimaryKey =
54 | ( id ∷ Column Int (Identity /\ Constraint (Composite "pk_double_primary_key") PrimaryKey)
55 | , second_id ∷ Column Int (Identity /\ Constraint (Composite "pk_double_primary_key") PrimaryKey)
56 | )
57 |
58 | type DoublePrimaryKeyTable = Table "double_primary_key" DoublePrimaryKey
59 |
60 | type Migrated =
61 | ( id ∷ Column Int PrimaryKey)
62 |
63 |
64 | type CompositeT =
65 | ( id ∷ Column Int (Identity /\ Constraint (Composite "pk_composite") PrimaryKey)
66 | , second_id ∷ Column Int (Constraint (Composite "pk_composite") PrimaryKey)
67 | , create ∷ Maybe Date
68 | , name ∷ String
69 | , sender ∷ Column Int (Constraint (Composite "sr_user") (ForeignKey "id" DoublePrimaryKeyTable))
70 | , recipient ∷ Column Int (Constraint (Composite "sr_user") (ForeignKey "second_id" DoublePrimaryKeyTable))
71 | )
72 |
73 | newtype SenderColumn = SenderColumn Int
74 | newtype RecipientColumn = RecipientColumn Int
75 |
76 | instance ToValue SenderColumn where
77 | toValue (SenderColumn x) = toValue x
78 |
79 | instance ToValue RecipientColumn where
80 | toValue (RecipientColumn x) = toValue x
81 |
82 | instance FromValue SenderColumn where
83 | fromValue x = SenderColumn <$> fromValue x
84 |
85 | instance FromValue RecipientColumn where
86 | fromValue x = RecipientColumn <$> fromValue x
87 |
88 | derive instance Eq SenderColumn
89 | derive instance Eq RecipientColumn
90 |
91 | instance Show SenderColumn where
92 | show (SenderColumn x) = show x
93 |
94 | instance Show RecipientColumn where
95 | show (RecipientColumn x) = show x
96 |
97 | users ∷ UsersTable
98 | users = Table
99 |
100 | messages ∷ Table "messages" Messages
101 | messages = Table
102 |
103 | tags ∷ Table "tags" Tags
104 | tags = Table
105 |
106 | maybeKeys ∷ Table "maybe_keys" MaybeKeys
107 | maybeKeys = Table
108 |
109 | uniqueValues ∷ Table "unique_values" UniqueValues
110 | uniqueValues = Table
111 |
112 | defaultColumns ∷ Table "default_columns" DefaultColumns
113 | defaultColumns = Table
114 |
115 | doublePrimaryKey ∷ DoublePrimaryKeyTable
116 | doublePrimaryKey = Table
117 |
118 | composite ∷ Table "composite" CompositeT
119 | composite = Table
120 |
121 | migrated ∷ Table "migrated" Migrated
122 | migrated = Table
123 |
124 | id ∷ Proxy "id"
125 | id = Proxy
126 |
127 | name ∷ Proxy "name"
128 | name = Proxy
129 |
130 | surname ∷ Proxy "surname"
131 | surname = Proxy
132 |
133 | sent ∷ Proxy "sent"
134 | sent = Proxy
135 |
136 | date ∷ Proxy "date"
137 | date = Proxy
138 |
139 | joined ∷ Proxy "joined"
140 | joined = Proxy
141 |
142 | birthday ∷ Proxy "birthday"
143 | birthday = Proxy
144 |
145 | sender ∷ Proxy "sender"
146 | sender = Proxy
147 |
148 | recipient ∷ Proxy "recipient"
149 | recipient = Proxy
150 |
151 | secondDate ∷ Proxy "second_date"
152 | secondDate = Proxy
153 |
154 | secondId ∷ Proxy "second_id"
155 | secondId = Proxy
156 |
157 | created ∷ Proxy "created"
158 | created = Proxy
159 |
160 | _by ∷ Proxy "by"
161 | _by = Proxy
162 |
163 | b ∷ Proxy "b"
164 | b = Proxy
165 |
166 | n ∷ Proxy "n"
167 | n = Proxy
168 |
169 | t ∷ Proxy "t"
170 | t = Proxy
171 |
172 | u ∷ Proxy "u"
173 | u = Proxy
174 |
175 | bigB ∷ Proxy "B"
176 | bigB = Proxy
177 |
178 | date_part_age ∷ FunctionSignature (String /\ DateTime) Int
179 | date_part_age = function "date_part_age"
180 |
181 | date_part_age' ∷ FunctionSignature (String /\ Date) Int
182 | date_part_age' = function "date_part_age"
183 |
184 | fire_missiles ∷ FunctionSignature (Int /\ Int) Unit
185 | fire_missiles = function "fire_missiles"
186 |
187 | utc_now ∷ FunctionSignature' DateTime
188 | utc_now = function' "utc_now"
--------------------------------------------------------------------------------
/test/Union.purs:
--------------------------------------------------------------------------------
1 | module Test.Union where
2 |
3 | import Droplet.Language (as, from, select, union, unionAll, wher, (.<>.), (.=.))
4 | import Prelude
5 | import Test.Types (b, id, messages, name, users)
6 |
7 | import Droplet.Language.Internal.Translate as DLIQ
8 | import Test.Model as TM
9 |
10 | import Test.Spec (Spec)
11 | import Test.Spec as TS
12 |
13 |
14 | tests ∷ Spec Unit
15 | tests = do
16 | TS.describe "union" do
17 | TS.it "select" do
18 | let q = (select id # from users # wher (name .=. "mary")) `union` (select id # from users # wher (name .<>. "mary"))
19 | TM.parameterized """(SELECT "id" FROM "users" WHERE "name" = $1 UNION SELECT "id" FROM "users" WHERE "name" <> $2)""" $ DLIQ.buildQuery q
20 | TM.result q [ { id: 2 }, { id: 1 } ]
21 | TS.it "as" do
22 | let q = (select id # from (users # as b)) `union` (select id # from users # wher (name .<>. "mary"))
23 | TM.parameterized """(SELECT "id" FROM "users" AS "b" UNION SELECT "id" FROM "users" WHERE "name" <> $1)""" $ DLIQ.buildQuery q
24 | TM.result q [ { id: 2 }, { id: 1 } ]
25 | TS.describe "from union" do
26 | TS.it "left" do
27 | let q = ((select id # from users) `union` (select id # from messages)) `union` (select id # from users)
28 | TM.notParameterized """((SELECT "id" FROM "users" UNION SELECT "id" FROM "messages") UNION SELECT "id" FROM "users")""" $ DLIQ.buildQuery q
29 | TM.result q [ { id: 2 }, { id: 1 } ]
30 | TS.it "right" do
31 | let q = (select id # from users) `union` ((select id # from messages) `union` (select id # from users))
32 | TM.notParameterized """(SELECT "id" FROM "users" UNION (SELECT "id" FROM "messages" UNION SELECT "id" FROM "users"))""" $ DLIQ.buildQuery q
33 | TM.result q [ { id: 2 }, { id: 1 } ]
34 |
35 | TS.describe "union all" do
36 | TS.it "select" do
37 | let q = (select id # from users # wher (name .=. "mary")) `unionAll` (select id # from users # wher (name .<>. "mary"))
38 | TM.parameterized """(SELECT "id" FROM "users" WHERE "name" = $1 UNION ALL SELECT "id" FROM "users" WHERE "name" <> $2)""" $ DLIQ.buildQuery q
39 | TM.result q [ { id: 2 }, { id: 1 } ]
40 | TS.it "as" do
41 | let q = (select id # from (users # as b)) `unionAll` (select id # from users # wher (name .<>. "mary"))
42 | TM.parameterized """(SELECT "id" FROM "users" AS "b" UNION ALL SELECT "id" FROM "users" WHERE "name" <> $1)""" $ DLIQ.buildQuery q
43 | TM.result q [ { id: 1 }, { id: 2 }, { id: 1 } ]
44 | TS.describe "from unionAll" do
45 | TS.it "left" do
46 | let q = ((select id # from users) `unionAll` (select id # from messages)) `unionAll` (select id # from users)
47 | TM.notParameterized """((SELECT "id" FROM "users" UNION ALL SELECT "id" FROM "messages") UNION ALL SELECT "id" FROM "users")""" $ DLIQ.buildQuery q
48 | TM.result q [ { id: 1 }, { id: 2 }, { id: 1 }, { id: 2 }, { id: 1 }, { id: 2 } ]
49 | TS.it "right" do
50 | let q = (select id # from users) `unionAll` ((select id # from messages) `unionAll` (select id # from users))
51 | TM.notParameterized """(SELECT "id" FROM "users" UNION ALL (SELECT "id" FROM "messages" UNION ALL SELECT "id" FROM "users"))""" $ DLIQ.buildQuery q
52 | TM.result q [ { id: 1 }, { id: 2 }, { id: 1 }, { id: 2 }, { id: 1 }, { id: 2 } ]
53 |
--------------------------------------------------------------------------------
/test/Unsafe.purs:
--------------------------------------------------------------------------------
1 | module Test.Unsafe where
2 |
3 | import Prelude
4 |
5 | import Data.Array ((!!))
6 | import Data.Array as DA
7 | import Data.Array.Partial as DAP
8 | import Data.Maybe (Maybe(..))
9 | import Data.Maybe as DM
10 | import Data.Tuple.Nested ((/\))
11 | import Droplet.Language.Internal.Translate (Query(..))
12 | import Droplet.Language.Internal.Translate as DLIQ
13 | import Foreign as F
14 | import Partial.Unsafe as PU
15 | import Test.Model as TM
16 |
17 | import Test.Spec (Spec)
18 | import Test.Spec as TS
19 | import Test.Spec.Assertions as TSA
20 |
21 |
22 | tests ∷ Spec Unit
23 | tests = do
24 | TS.describe "unsafe queries" do
25 | TS.it "select" do
26 | let (plan /\ q /\ pr) = Nothing /\ """SELECT "name", m.id FROM "users" u JOIN "messages" m on "u"."id" = m.sender WHERE "u"."id" = @id OR "u"."id" = @id2 OR "u"."id" = @id3""" /\ { id: 2, id2: 3, id3: 4 }
27 | let Query _ dollaredQ parameters = DLIQ.unsafeBuildQuery plan q pr
28 | --parameters are replaced by field order
29 | TSA.shouldEqual """SELECT "name", m.id FROM "users" u JOIN "messages" m on "u"."id" = m.sender WHERE "u"."id" = $1 OR "u"."id" = $2 OR "u"."id" = $3""" dollaredQ
30 | TSA.shouldEqual 3 $ DA.length parameters
31 | TSA.shouldEqual pr.id <<< F.unsafeFromForeign $ PU.unsafePartial (DAP.head parameters)
32 | TSA.shouldEqual pr.id2 <<< F.unsafeFromForeign $ PU.unsafePartial (DM.fromJust $ parameters !! 1)
33 | TSA.shouldEqual pr.id3 <<< F.unsafeFromForeign $ PU.unsafePartial (DM.fromJust $ parameters !! 2)
34 | TM.unsafeResult plan q pr [ { name: "mary", id: 2 } ]
35 | TS.it "insert" do
36 | let (plan /\ q) = Nothing /\ "insert into tags(name) values('hey')"
37 | let Query _ dollaredQ parameters = DLIQ.unsafeBuildQuery plan q {}
38 | TSA.shouldEqual "insert into tags(name) values('hey')" dollaredQ
39 | TSA.shouldEqual 0 $ DA.length parameters
40 | TM.unsafeResult plan q {} ([] ∷ Array {})
41 | TS.it "update" do
42 | let (plan /\ q /\ pr) = Nothing /\ """UPDATE users SET "name" = @name WHERE "id" = @id""" /\ { name: "Suzy", id: 23 }
43 | let Query _ dollaredQ parameters = DLIQ.unsafeBuildQuery plan q pr
44 | --parameters are replaced by field order
45 | TSA.shouldEqual """UPDATE users SET "name" = $2 WHERE "id" = $1""" dollaredQ
46 | TSA.shouldEqual 2 $ DA.length parameters
47 | TSA.shouldEqual pr.id <<< F.unsafeFromForeign $ PU.unsafePartial (DAP.head parameters)
48 | TSA.shouldEqual pr.name <<< F.unsafeFromForeign $ PU.unsafePartial (DM.fromJust $ parameters !! 1)
49 | TM.unsafeResult plan q pr ([] ∷ Array {})
50 | TS.it "delete" do
51 | let (plan /\ q /\ pr) = Nothing /\ """DELETE FROM "users" WHERE joined = @joined""" /\ { joined: TM.makeDate 2000 1 1 }
52 | let Query _ dollaredQ parameters = DLIQ.unsafeBuildQuery plan q pr
53 | --parameters are replaced by field order
54 | TSA.shouldEqual """DELETE FROM "users" WHERE joined = $1""" dollaredQ
55 | TSA.shouldEqual 1 $ DA.length parameters
56 | TM.unsafeResult plan q pr ([] ∷ Array {})
57 |
--------------------------------------------------------------------------------
/test/Update.purs:
--------------------------------------------------------------------------------
1 | module Test.Update where
2 |
3 | import Droplet.Language
4 | import Droplet.Language
5 | import Prelude
6 | import Test.Types
7 |
8 | import Data.Tuple.Nested ((/\))
9 | import Droplet.Language.Internal.Translate as DLIQ
10 | import Test.Model as TM
11 |
12 | import Test.Spec (Spec)
13 | import Test.Spec as TS
14 |
15 | tests ∷ Spec Unit
16 | tests = TS.describe "setup" do
17 | TS.describe "set" do
18 | TS.it "single field" do
19 | let q = update users # set (surname .=. "Sue")
20 | TM.parameterized """UPDATE "users" SET surname = $1""" $ DLIQ.buildQuery q
21 | TM.result q []
22 | TS.it "fields" do
23 | let q = update users # set ((name .=. "Mary") /\ (surname .=. "Sue"))
24 | TM.parameterized """UPDATE "users" SET name = $1, surname = $2""" $ DLIQ.buildQuery q
25 | TM.result q []
26 | TS.it "default" do
27 | let q = update users # set ((name .=. "Mary") /\ (birthday .=. Default))
28 | TM.parameterized """UPDATE "users" SET name = $1, birthday = DEFAULT""" $ DLIQ.buildQuery q
29 | TM.result q []
30 | TS.describe "where" do
31 | TS.it "single field" do
32 | let q = update users # set (surname .=. "Sue") # wher (id .=. 1)
33 | TM.parameterized """UPDATE "users" SET surname = $1 WHERE "id" = $2""" $ DLIQ.buildQuery q
34 | TM.result q []
35 | TS.it "fields" do
36 | let q = update users # set ((name .=. "Mary") /\ (surname .=. "Sue")) # wher (id .=. 2 .||. id .=. 4)
37 | TM.parameterized """UPDATE "users" SET name = $1, surname = $2 WHERE ("id" = $3 OR "id" = $4)""" $ DLIQ.buildQuery q
38 | TM.result q []
39 |
--------------------------------------------------------------------------------
/test/Where.purs:
--------------------------------------------------------------------------------
1 | module Test.Where where
2 |
3 | import Droplet.Language
4 | import Prelude hiding (not, join)
5 | import Test.Types
6 |
7 | import Data.Array.NonEmpty as DAN
8 | import Data.Maybe (Maybe(..))
9 | import Data.NonEmpty (NonEmpty(..))
10 | import Data.Tuple.Nested ((/\))
11 | import Droplet.Language.Internal.Translate as DLIQ
12 | import Test.Model as TM
13 | import Test.Spec (Spec)
14 | import Test.Spec as TS
15 | import Type.Proxy (Proxy(..))
16 |
17 | tests ∷ Spec Unit
18 | tests = do
19 | TS.describe "where" do
20 | TS.describe "compared to parameter" do
21 | TS.it "equals" do
22 | let q = select recipient # from messages # wher (sender .=. 1)
23 | TM.parameterized """SELECT "recipient" FROM "messages" WHERE "sender" = $1""" $ DLIQ.buildQuery q
24 | TM.result q [ { recipient: 2 } ]
25 | TS.it "not equals" do
26 | let q = select sender # from messages # wher (recipient .<>. 2)
27 | TM.parameterized """SELECT "sender" FROM "messages" WHERE "recipient" <> $1""" $ DLIQ.buildQuery q
28 | TM.result q [ { sender: 2 } ]
29 | TS.it "lesser than" do
30 | let q = select sender # from messages # wher (recipient .<. 2)
31 | TM.parameterized """SELECT "sender" FROM "messages" WHERE "recipient" < $1""" $ DLIQ.buildQuery q
32 | TM.result q [ { sender: 2 } ]
33 | TS.it "lesser equals than" do
34 | let q = select sender # from messages # wher (recipient .<=. 2)
35 | TM.parameterized """SELECT "sender" FROM "messages" WHERE "recipient" <= $1""" $ DLIQ.buildQuery q
36 | TM.result q [ { sender: 1}, { sender: 2 } ]
37 | TS.it "nullable" do
38 | let q = select _by # from tags # wher (_by .=. 2)
39 | TM.parameterized """SELECT "by" FROM "tags" WHERE "by" = $1""" $ DLIQ.buildQuery q
40 | TM.result q [ ]
41 |
42 | TS.describe "compared to field" do
43 | TS.it "equals" do
44 | let q = select (34 # as n) # from users # wher (name .=. surname)
45 | TM.notParameterized """SELECT 34 AS "n" FROM "users" WHERE "name" = "surname"""" $ DLIQ.buildQuery q
46 | TM.result q []
47 | TS.it "not equals" do
48 | let q = select (34 # as n) # from users # wher (name .<>. surname)
49 | TM.notParameterized """SELECT 34 AS "n" FROM "users" WHERE "name" <> "surname"""" $ DLIQ.buildQuery q
50 | TM.result q [ { n: 34 }, { n: 34 } ]
51 | TS.it "greater than" do
52 | let q = select sender # from messages # wher (recipient .>. 2)
53 | TM.parameterized """SELECT "sender" FROM "messages" WHERE "recipient" > $1""" $ DLIQ.buildQuery q
54 | TM.result q []
55 | TS.it "greater equals than" do
56 | let q = select sender # from messages # wher (recipient .>=. 2)
57 | TM.parameterized """SELECT "sender" FROM "messages" WHERE "recipient" >= $1""" $ DLIQ.buildQuery q
58 | TM.result q [{sender: 1}]
59 |
60 | TS.describe "logical operands" do
61 | TS.describe "and" do
62 | TS.it "single" do
63 | let q = select id # from users # wher (name .=. "josh" .&&. name .<>. surname)
64 | TM.parameterized """SELECT "id" FROM "users" WHERE ("name" = $1 AND "name" <> "surname")""" $ DLIQ.buildQuery q
65 | TM.result q [ { id: 1 } ]
66 | TS.it "many" do
67 | let q = select id # from users # wher (name .=. "josh" .&&. "josh" .=. name .&&. surname .=. "j.")
68 | TM.parameterized """SELECT "id" FROM "users" WHERE (("name" = $1 AND $2 = "name") AND "surname" = $3)""" $ DLIQ.buildQuery q
69 | TM.result q [ { id: 1 } ]
70 |
71 | TS.describe "or" do
72 | TS.it "single" do
73 | let q = select id # from users # wher (name .=. "mary" .||. name .=. surname)
74 | TM.parameterized """SELECT "id" FROM "users" WHERE ("name" = $1 OR "name" = "surname")""" $ DLIQ.buildQuery q
75 | TM.result q [ { id: 2 } ]
76 | TS.it "many" do
77 | let q = select id # from users # wher (name .=. "josh" .||. name .=. "j." .||. surname .<>. "josh")
78 | TM.parameterized """SELECT "id" FROM "users" WHERE (("name" = $1 OR "name" = $2) OR "surname" <> $3)""" $ DLIQ.buildQuery q
79 | TM.result q [ { id: 1 }, { id: 2 } ]
80 |
81 | TS.it "in" do
82 | let q = select id # from users # wher (id `in_` DAN.fromNonEmpty (NonEmpty 3 [4, 5]))
83 | TM.parameterized """SELECT "id" FROM "users" WHERE "id" IN ($1, $2, $3)""" $ DLIQ.buildQuery q
84 | TM.result q []
85 |
86 | TS.describe "is null" do
87 | TS.it "maybe field" do
88 | let q = select id # from tags # wher (_by # isNull)
89 | TM.notParameterized """SELECT "id" FROM "tags" WHERE "by" IS NULL""" $ DLIQ.buildQuery q
90 | TM.result q [ ]
91 | TS.it "joined field" do
92 | let q = select joined # from (leftJoin users (tags # as t) # on (joined .=. created)) # wher (isNull (t ... id))
93 | TM.notParameterized """SELECT "joined" FROM "users" LEFT JOIN "tags" AS "t" ON "joined" = "created" WHERE "t"."id" IS NULL""" $ DLIQ.buildQuery q
94 | TM.result' q []
95 |
96 | TS.describe "is not null" do
97 | TS.it "maybe field" do
98 | let q = select id # from tags # wher (_by # isNotNull)
99 | TM.notParameterized """SELECT "id" FROM "tags" WHERE "by" IS NOT NULL""" $ DLIQ.buildQuery q
100 | TM.result q [ { id: 1 } ]
101 | TS.it "joined field" do
102 | let q = select joined # from (leftJoin users (tags # as t) # on (joined .=. created)) # wher (isNotNull (t ... id))
103 | TM.notParameterized """SELECT "joined" FROM "users" LEFT JOIN "tags" AS "t" ON "joined" = "created" WHERE "t"."id" IS NOT NULL""" $ DLIQ.buildQuery q
104 | TM.result q [ ]
105 |
106 | TS.describe "not" do
107 | TS.it "operator" do
108 | let q = select id # from users # wher (not (id .<>. 5))
109 | TM.parameterized """SELECT "id" FROM "users" WHERE NOT "id" <> $1""" $ DLIQ.buildQuery q
110 | TM.result q []
111 | TS.it "and" do
112 | let q = select id # from (users # as u) # wher (not (id .<>. 5) .&&. id .=. 1)
113 | TM.parameterized """SELECT "id" FROM "users" AS "u" WHERE (NOT "id" <> $1 AND "id" = $2)""" $ DLIQ.buildQuery q
114 | TM.result q []
115 | TS.it "or" do
116 | let q = select id # from (users # as u) # wher (not (id .<>. 5 .||. id .=. 1))
117 | TM.parameterized """SELECT "id" FROM "users" AS "u" WHERE NOT ("id" <> $1 OR "id" = $2)""" $ DLIQ.buildQuery q
118 | TM.result q []
119 | TS.it "exists" do
120 | let q = select id # from (users # as u) # wher (not $ exists $ select id # from users)
121 | TM.notParameterized """SELECT "id" FROM "users" AS "u" WHERE NOT EXISTS (SELECT "id" FROM "users")""" $ DLIQ.buildQuery q
122 | TM.result q []
123 |
124 | TS.describe "exists" do
125 | TS.it "column" do
126 | let q = select id # from users # wher (exists $ select id # from users)
127 | TM.notParameterized """SELECT "id" FROM "users" WHERE EXISTS (SELECT "id" FROM "users")""" $ DLIQ.buildQuery q
128 | TM.result q [ { id: 1 }, { id: 2 } ]
129 | TS.describe "path" do
130 | TS.it "outer" do
131 | let q = select id # from (users # as u) # wher (exists $ select (u ... id) # from users)
132 | TM.notParameterized """SELECT "id" FROM "users" AS "u" WHERE EXISTS (SELECT "u"."id" "u.id" FROM "users")""" $ DLIQ.buildQuery q
133 | TM.result q [ { id: 1 }, { id: 2 } ]
134 | TS.it "outer where" do
135 | let q = select id # from (users # as u) # wher (exists $ select (3 # as t) # from users # wher (id .=. u ... id))
136 | TM.notParameterized """SELECT "id" FROM "users" AS "u" WHERE EXISTS (SELECT 3 AS "t" FROM "users" WHERE "id" = "u"."id")""" $ DLIQ.buildQuery q
137 | TM.result q [ { id: 1 }, { id: 2 } ]
138 | TS.it "outer inner where" do
139 | let q = select id # from (users # as u) # wher (exists $ select id # from (users # as t) # wher (t ... id .=. u ... id))
140 | TM.notParameterized """SELECT "id" FROM "users" AS "u" WHERE EXISTS (SELECT "id" FROM "users" AS "t" WHERE "t"."id" = "u"."id")""" $ DLIQ.buildQuery q
141 | TM.result q [ { id: 1 }, { id: 2 } ]
142 |
143 | TS.describe "mixed" do
144 | TS.it "not bracketed" do
145 | let q = select id # from users # wher (id .=. 333 .||. id .=. 33 .&&. id .=. 3)
146 | TM.parameterized """SELECT "id" FROM "users" WHERE ("id" = $1 OR ("id" = $2 AND "id" = $3))""" $ DLIQ.buildQuery q
147 | TM.result q []
148 | TS.it "bracketed" do
149 | let q = select id # from users # wher ((id .=. 2 .||. id .=. 22) .&&. id .=. 2)
150 | TM.parameterized """SELECT "id" FROM "users" WHERE (("id" = $1 OR "id" = $2) AND "id" = $3)""" $ DLIQ.buildQuery q
151 | TM.result q [ { id: 2 } ]
152 | TS.it "with exists" do
153 | let q = select id # from users # wher ((id .=. 2 .||. (exists $ select id # from users)) .&&. id .=. 2)
154 | TM.parameterized """SELECT "id" FROM "users" WHERE (("id" = $1 OR EXISTS (SELECT "id" FROM "users")) AND "id" = $2)""" $ DLIQ.buildQuery q
155 | TM.result q [ { id: 2 } ]
156 | TS.it "with not" do
157 | let q = select id # from users # wher ((id .=. 2 .||. not (exists $ select id # from users)) .&&. not (id .=. 2))
158 | TM.parameterized """SELECT "id" FROM "users" WHERE (("id" = $1 OR NOT EXISTS (SELECT "id" FROM "users")) AND NOT "id" = $2)""" $ DLIQ.buildQuery q
159 | TM.result q []
160 |
161 | TS.describe "subqueries" do
162 | TS.it "scalar" do
163 | let namep = "mary"
164 | let q = select (select (4 # as n) # from users # wher (name .=. namep) # orderBy id # limit (Proxy :: _ 1) # as b)
165 | TM.parameterized """SELECT (SELECT 4 AS "n" FROM "users" WHERE "name" = $1 ORDER BY "id" LIMIT 1) AS "b"""" $ DLIQ.buildQuery q
166 | TM.result q [ { b: Just 4 } ]
167 | TS.it "field" do
168 | let namep = "josh"
169 | let q = select (select id # from users # wher (name .=. namep) # orderBy id # limit (Proxy :: _ 1) # as b)
170 | TM.parameterized """SELECT (SELECT "id" FROM "users" WHERE "name" = $1 ORDER BY "id" LIMIT 1) AS "b"""" $ DLIQ.buildQuery q
171 | TM.result q [ { b: Just 1 } ]
172 | TS.it "tuple" do
173 | let parameters = { d: "mary", e: 2 }
174 | let q = select ((3 # as (Proxy ∷ Proxy "e")) /\ (select id # from users # wher (name .=. parameters.d) # orderBy id # limit (Proxy :: _ 1) # as b) /\ (select id # from messages # wher (id .=. parameters.e) # orderBy id # limit (Proxy :: _ 1) # as n))
175 | TM.parameterized """SELECT 3 AS "e", (SELECT "id" FROM "users" WHERE "name" = $1 ORDER BY "id" LIMIT 1) AS "b", (SELECT "id" FROM "messages" WHERE "id" = $2 ORDER BY "id" LIMIT 1) AS "n"""" $ DLIQ.buildQuery q
176 | TM.result q [ { e: 3, b: Just 2, n: Just 2 } ]
177 | TS.it "where" do
178 | let parameters = { d: "mary", e: 2 }
179 | let q = select ((3 # as (Proxy ∷ Proxy "e")) /\ (select id # from users # wher (name .=. parameters.d) # orderBy id # limit (Proxy :: _ 1) # as b) /\ (select id # from messages # wher (id .=. parameters.e) # orderBy id # limit (Proxy :: _ 1) # as n)) # from users # wher (id .=. 1 .||. id .=. 2)
180 | TM.parameterized """SELECT 3 AS "e", (SELECT "id" FROM "users" WHERE "name" = $1 ORDER BY "id" LIMIT 1) AS "b", (SELECT "id" FROM "messages" WHERE "id" = $2 ORDER BY "id" LIMIT 1) AS "n" FROM "users" WHERE ("id" = $3 OR "id" = $4)""" $ DLIQ.buildQuery q
181 | TM.result q [ { e: 3, b: Just 2, n: Just 2 }, { e: 3, b: Just 2, n: Just 2 } ]
182 |
183 | TS.describe "references" do
184 | TS.it "named table" do
185 | let q = select recipient # from (messages # as u) # wher (sender .=. 1)
186 | TM.parameterized """SELECT "recipient" FROM "messages" AS "u" WHERE "sender" = $1""" $ DLIQ.buildQuery q
187 | TM.result q [ { recipient: 2 } ]
188 | TS.it "named table alias" do
189 | let q = select (u ... id) # from (users # as u) # wher (u ... id .<>. 3)
190 | TM.parameterized """SELECT "u"."id" "u.id" FROM "users" AS "u" WHERE "u"."id" <> $1""" $ DLIQ.buildQuery q
191 | TM.result q [ { "u.id": 1 }, { "u.id": 2 } ]
192 | TS.it "named subquery" do
193 | let q = select n # from (select (id # as n) # from users # as u) # wher (u ... n .<>. 3)
194 | TM.parameterized """SELECT "n" FROM (SELECT "id" AS "n" FROM "users") AS "u" WHERE "u"."n" <> $1""" $ DLIQ.buildQuery q
195 | TM.result q [ { n: 1 }, { n: 2 } ]
196 | TS.describe "column subquery" do
197 | TS.it "outer" do
198 | let q = select (select id # from users # wher (u ... id .<>. id) # orderBy id # limit (Proxy :: _ 1)) # from (users # as u)
199 | TM.notParameterized """SELECT (SELECT "id" FROM "users" WHERE "u"."id" <> "id" ORDER BY "id" LIMIT 1) FROM "users" AS "u"""" $ DLIQ.buildQuery q
200 | TM.result q [ { id: Just 2 }, { id: Just 1 } ]
201 | TS.it "value" do
202 | let q = select (id /\ (select ((u ... id) # as n) # from users # wher (id .=. 2) # orderBy id # limit (Proxy :: _ 1))) # from (users # as u)
203 | TM.parameterized """SELECT "id", (SELECT "u"."id" AS "n" FROM "users" WHERE "id" = $1 ORDER BY "id" LIMIT 1) FROM "users" AS "u"""" $ DLIQ.buildQuery q
204 | TM.result q [ { id: 1, n: Just 1 }, { id: 2, n: Just 2 } ]
205 | TS.it "inner alias" do
206 | let q = select (select (n ... name) # from (users # as n) # wher (u ... id .<>. n ... id) # orderBy id # limit (Proxy :: _ 1)) # from (users # as u)
207 | TM.notParameterized """SELECT (SELECT "n"."name" "n.name" FROM "users" AS "n" WHERE "u"."id" <> "n"."id" ORDER BY "id" LIMIT 1) FROM "users" AS "u"""" $ DLIQ.buildQuery q
208 | TM.result q [ { "n.name": Just "mary" }, { "n.name": Just "josh" } ]
209 | TS.it "same alias" do
210 | let q = select (id /\ (select (u ... id) # from (users # as u) # wher (u ... id .<>. u ... id) # orderBy id # limit (Proxy :: _ 1))) # from (users # as u)
211 | TM.notParameterized """SELECT "id", (SELECT "u"."id" "u.id" FROM "users" AS "u" WHERE "u"."id" <> "u"."id" ORDER BY "id" LIMIT 1) FROM "users" AS "u"""" $ DLIQ.buildQuery q
212 | TM.result q [ { id: 1, "u.id": Nothing }, { id: 2, "u.id": Nothing } ]
213 | TS.it "join" do
214 | let q = select ((select name # from users # wher (id .=. u ... _by) # orderBy id # limit (Proxy :: _ 1)) # as b) # from (join (tags # as u) (messages # as b) # on (u ... id .=. b ... id)) # wher (u ... id .=. 34)
215 | TM.parameterized """SELECT (SELECT "name" FROM "users" WHERE "id" = "u"."by" ORDER BY "id" LIMIT 1) AS "b" FROM "tags" AS "u" INNER JOIN "messages" AS "b" ON "u"."id" = "b"."id" WHERE "u"."id" = $1""" $ DLIQ.buildQuery q
216 | TM.result q []
--------------------------------------------------------------------------------
/test/sql/index.sql:
--------------------------------------------------------------------------------
1 | create or replace function utc_now()
2 | returns timestamptz as
3 | $body$
4 | begin
5 | return now() at time zone 'utc';
6 | end;
7 | $body$
8 | language plpgsql;
9 |
10 | create or replace function recipient_default()
11 | returns integer as
12 | $body$
13 | begin
14 | return (select coalesce(max(recipient), 1) from default_columns); -- don't try it at home
15 | end;
16 | $body$
17 | language plpgsql;
18 |
19 | create table users (
20 | id integer generated always as identity primary key,
21 | name text not null,
22 | surname text not null,
23 | birthday date default (utc_now()),
24 | joined date default (utc_now())
25 | );
26 |
27 | create table messages (
28 | id integer generated always as identity primary key,
29 | sender integer not null,
30 | recipient integer not null,
31 | date timestamp without time zone default (utc_now()),
32 | second_date timestamp with time zone default (utc_now()),
33 | sent bool not null,
34 |
35 | constraint sender_user foreign key (sender) references users(id),
36 | constraint recipient_user foreign key (recipient) references users(id)
37 | );
38 |
39 | create table tags (
40 | id integer generated always as identity primary key,
41 | name text not null,
42 | created date,
43 | by integer,
44 |
45 | constraint by_user foreign key (by) references users(id)
46 | );
47 |
48 | create table maybe_keys (
49 | id integer primary key
50 | );
51 |
52 | create table unique_values (
53 | name text not null unique,
54 | by integer unique
55 | );
56 |
57 | create table default_columns (
58 | recipient integer unique not null default (recipient_default()),
59 | sender integer default (44)
60 | );
61 |
62 | create table double_primary_key (
63 | id integer generated always as identity,
64 | second_id integer generated always as identity ,
65 |
66 | constraint pk_double_primary_key primary key (id, second_id)
67 | );
68 |
69 | create table composite (
70 | id integer generated always as identity,
71 | second_id integer not null,
72 | name text not null,
73 | created date,
74 | sender integer,
75 | recipient integer,
76 |
77 | constraint pk_composite primary key (id, second_id),
78 | constraint sr_user foreign key (sender, recipient) references double_primary_key(id, second_id )
79 | );
80 |
81 | create or replace function truncate_tables()
82 | returns void as
83 | $body$
84 | begin
85 | truncate table users restart identity cascade;
86 | truncate table messages restart identity cascade;
87 | truncate table tags restart identity cascade;
88 | truncate table maybe_keys cascade;
89 | truncate table unique_values cascade;
90 | truncate table default_columns cascade;
91 | truncate table double_primary_key restart identity cascade;
92 | truncate table composite restart identity cascade;
93 | drop table if exists test;
94 | drop table if exists migrated;
95 | drop table if exists __droplet_migrations__;
96 | end;
97 | $body$
98 | language plpgsql;
99 |
100 | create or replace function date_part_age(part text, tm timestamp with time zone)
101 | returns integer as
102 | $body$
103 | begin
104 | return date_part(part, age(now (), tm));
105 | end;
106 | $body$
107 | language plpgsql;
108 |
109 | create or replace function fire_missiles(a integer, b integer)
110 | returns void as
111 | $body$
112 | begin
113 |
114 | end;
115 | $body$
116 | language plpgsql;
--------------------------------------------------------------------------------