git.iko.soy/typeable/postgresql-simple
on master help sign in
42 files changed+1330−552
since splitsnapshots
unifiedsplit
.ghci−16 .github/workflows/haskell-ci.yml+256 .github/workflows/simple.yml+58 .gitignore+2 .travis.yml−129 CHANGES.md+82−7 CONTRIBUTING.md+13 README.md−3 cabal.haskell-ci+5 cabal.project+2 cabal.project.local.win+4 postgresql-simple.cabal+129−146 src/Database/PostgreSQL/Simple.hs+22−10 src/Database/PostgreSQL/Simple/Arrays.hs+2−2 src/Database/PostgreSQL/Simple/Compat.hs−53 src/Database/PostgreSQL/Simple/Copy.hs+27−6 src/Database/PostgreSQL/Simple/Cursor.hs+5−5 src/Database/PostgreSQL/Simple/Errors.hs+4−3 src/Database/PostgreSQL/Simple/FromField.hs+70−33 src/Database/PostgreSQL/Simple/FromRow.hs+2−2 src/Database/PostgreSQL/Simple/HStore/Implementation.hs+3−9 src/Database/PostgreSQL/Simple/Internal.hs+59−13 src/Database/PostgreSQL/Simple/Internal/PQResultUtils.hs+2−6 src/Database/PostgreSQL/Simple/Newtypes.hs+49 src/Database/PostgreSQL/Simple/Notification.hs−17 src/Database/PostgreSQL/Simple/Ok.hs+7−4 src/Database/PostgreSQL/Simple/Range.hs+7−7 src/Database/PostgreSQL/Simple/SqlQQ.hs+1−1 src/Database/PostgreSQL/Simple/Time.hs+2 src/Database/PostgreSQL/Simple/Time/Implementation.hs+11−1 src/Database/PostgreSQL/Simple/Time/Internal/Parser.hs+12−3 src/Database/PostgreSQL/Simple/Time/Internal/Printer.hs+14−2 src/Database/PostgreSQL/Simple/ToField.hs+22−11 src/Database/PostgreSQL/Simple/Transaction.hs+16−12 src/Database/PostgreSQL/Simple/TypeInfo.hs+9−9 src/Database/PostgreSQL/Simple/Types.hs−7 test/Exception.hs+70 test/Inspection.hs+6−1 test/Interval.hs+176 test/Main.hs+179−32 test/Time.hs+1−1 test/results/malformed-input.expected+1−1
.ghcideleted−16
@@ -1,16 +0,0 @@
1−:set -isrc -itest -optP-includedist/build/autogen/cabal_macros.h
2−:set -Wall -fno-warn-name-shadowing
3−
4−:set -XDoAndIfThenElse
5−:set -XOverloadedStrings
6−:set -XBangPatterns
7−:set -XViewPatterns
8−:set -XTypeOperators
9−
10−-- for tests
11−:set -XNamedFieldPuns
12−:set -XRank2Types
13−:set -XRecordWildCards
14−:set -XPatternGuards
15−:set -XScopedTypeVariables
16−:set -fno-warn-unused-do-bind
.github/workflows/haskell-ci.ymlnew file+256
@@ -0,0 +1,256 @@
1+# This GitHub workflow config has been generated by a script via
2+#
3+# haskell-ci 'github' 'cabal.project'
4+#
5+# To regenerate the script (for example after adjusting tested-with) run
6+#
7+# haskell-ci regenerate
8+#
9+# For more information, see https://github.com/haskell-CI/haskell-ci
10+#
11+# version: 0.17.20231110
12+#
13+# REGENDATA ("0.17.20231110",["github","cabal.project"])
14+#
15+name: Haskell-CI
16+on:
17+ push:
18+ branches:
19+ - master
20+ pull_request:
21+ branches:
22+ - master
23+jobs:
24+ linux:
25+ name: Haskell-CI - Linux - ${{ matrix.compiler }}
26+ runs-on: ubuntu-20.04
27+ timeout-minutes:
28+ 60
29+ container:
30+ image: buildpack-deps:bionic
31+ services:
32+ postgres:
33+ image: postgres:14
34+ env:
35+ POSTGRES_PASSWORD: postgres
36+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
37+ continue-on-error: ${{ matrix.allow-failure }}
38+ strategy:
39+ matrix:
40+ include:
41+ - compiler: ghc-9.8.1
42+ compilerKind: ghc
43+ compilerVersion: 9.8.1
44+ setup-method: ghcup
45+ allow-failure: false
46+ - compiler: ghc-9.6.3
47+ compilerKind: ghc
48+ compilerVersion: 9.6.3
49+ setup-method: ghcup
50+ allow-failure: false
51+ - compiler: ghc-9.4.8
52+ compilerKind: ghc
53+ compilerVersion: 9.4.8
54+ setup-method: ghcup
55+ allow-failure: false
56+ - compiler: ghc-9.2.8
57+ compilerKind: ghc
58+ compilerVersion: 9.2.8
59+ setup-method: ghcup
60+ allow-failure: false
61+ - compiler: ghc-9.0.2
62+ compilerKind: ghc
63+ compilerVersion: 9.0.2
64+ setup-method: ghcup
65+ allow-failure: false
66+ - compiler: ghc-8.10.7
67+ compilerKind: ghc
68+ compilerVersion: 8.10.7
69+ setup-method: ghcup
70+ allow-failure: false
71+ - compiler: ghc-8.8.4
72+ compilerKind: ghc
73+ compilerVersion: 8.8.4
74+ setup-method: hvr-ppa
75+ allow-failure: false
76+ - compiler: ghc-8.6.5
77+ compilerKind: ghc
78+ compilerVersion: 8.6.5
79+ setup-method: hvr-ppa
80+ allow-failure: false
81+ fail-fast: false
82+ steps:
83+ - name: apt
84+ run: |
85+ apt-get update
86+ apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5
87+ if [ "${{ matrix.setup-method }}" = ghcup ]; then
88+ mkdir -p "$HOME/.ghcup/bin"
89+ curl -sL https://downloads.haskell.org/ghcup/0.1.20.0/x86_64-linux-ghcup-0.1.20.0 > "$HOME/.ghcup/bin/ghcup"
90+ chmod a+x "$HOME/.ghcup/bin/ghcup"
91+ "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false)
92+ "$HOME/.ghcup/bin/ghcup" install cabal 3.10.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
93+ else
94+ apt-add-repository -y 'ppa:hvr/ghc'
95+ apt-get update
96+ apt-get install -y "$HCNAME"
97+ mkdir -p "$HOME/.ghcup/bin"
98+ curl -sL https://downloads.haskell.org/ghcup/0.1.20.0/x86_64-linux-ghcup-0.1.20.0 > "$HOME/.ghcup/bin/ghcup"
99+ chmod a+x "$HOME/.ghcup/bin/ghcup"
100+ "$HOME/.ghcup/bin/ghcup" install cabal 3.10.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
101+ fi
102+ env:
103+ HCKIND: ${{ matrix.compilerKind }}
104+ HCNAME: ${{ matrix.compiler }}
105+ HCVER: ${{ matrix.compilerVersion }}
106+ - name: Set PATH and environment variables
107+ run: |
108+ echo "$HOME/.cabal/bin" >> $GITHUB_PATH
109+ echo "LANG=C.UTF-8" >> "$GITHUB_ENV"
110+ echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV"
111+ echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV"
112+ HCDIR=/opt/$HCKIND/$HCVER
113+ if [ "${{ matrix.setup-method }}" = ghcup ]; then
114+ HC=$("$HOME/.ghcup/bin/ghcup" whereis ghc "$HCVER")
115+ HCPKG=$(echo "$HC" | sed 's#ghc$#ghc-pkg#')
116+ HADDOCK=$(echo "$HC" | sed 's#ghc$#haddock#')
117+ echo "HC=$HC" >> "$GITHUB_ENV"
118+ echo "HCPKG=$HCPKG" >> "$GITHUB_ENV"
119+ echo "HADDOCK=$HADDOCK" >> "$GITHUB_ENV"
120+ echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.2.0 -vnormal+nowrap" >> "$GITHUB_ENV"
121+ else
122+ HC=$HCDIR/bin/$HCKIND
123+ echo "HC=$HC" >> "$GITHUB_ENV"
124+ echo "HCPKG=$HCDIR/bin/$HCKIND-pkg" >> "$GITHUB_ENV"
125+ echo "HADDOCK=$HCDIR/bin/haddock" >> "$GITHUB_ENV"
126+ echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.2.0 -vnormal+nowrap" >> "$GITHUB_ENV"
127+ fi
128+
129+ HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
130+ echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV"
131+ echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV"
132+ echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV"
133+ echo "HEADHACKAGE=false" >> "$GITHUB_ENV"
134+ echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV"
135+ echo "GHCJSARITH=0" >> "$GITHUB_ENV"
136+ env:
137+ HCKIND: ${{ matrix.compilerKind }}
138+ HCNAME: ${{ matrix.compiler }}
139+ HCVER: ${{ matrix.compilerVersion }}
140+ - name: env
141+ run: |
142+ env
143+ - name: write cabal config
144+ run: |
145+ mkdir -p $CABAL_DIR
146+ cat >> $CABAL_CONFIG <<EOF
147+ remote-build-reporting: anonymous
148+ write-ghc-environment-files: never
149+ remote-repo-cache: $CABAL_DIR/packages
150+ logs-dir: $CABAL_DIR/logs
151+ world-file: $CABAL_DIR/world
152+ extra-prog-path: $CABAL_DIR/bin
153+ symlink-bindir: $CABAL_DIR/bin
154+ installdir: $CABAL_DIR/bin
155+ build-summary: $CABAL_DIR/logs/build.log
156+ store-dir: $CABAL_DIR/store
157+ install-dirs user
158+ prefix: $CABAL_DIR
159+ repository hackage.haskell.org
160+ url: http://hackage.haskell.org/
161+ EOF
162+ cat >> $CABAL_CONFIG <<EOF
163+ program-default-options
164+ ghc-options: $GHCJOBS +RTS -M3G -RTS
165+ EOF
166+ cat $CABAL_CONFIG
167+ - name: versions
168+ run: |
169+ $HC --version || true
170+ $HC --print-project-git-commit-id || true
171+ $CABAL --version || true
172+ - name: update cabal index
173+ run: |
174+ $CABAL v2-update -v
175+ - name: install cabal-plan
176+ run: |
177+ mkdir -p $HOME/.cabal/bin
178+ curl -sL https://github.com/haskell-hvr/cabal-plan/releases/download/v0.7.3.0/cabal-plan-0.7.3.0-x86_64-linux.xz > cabal-plan.xz
179+ echo 'f62ccb2971567a5f638f2005ad3173dba14693a45154c1508645c52289714cb2 cabal-plan.xz' | sha256sum -c -
180+ xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan
181+ rm -f cabal-plan.xz
182+ chmod a+x $HOME/.cabal/bin/cabal-plan
183+ cabal-plan --version
184+ - name: checkout
185+ uses: actions/checkout@v3
186+ with:
187+ path: source
188+ - name: initial cabal.project for sdist
189+ run: |
190+ touch cabal.project
191+ echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project
192+ cat cabal.project
193+ - name: sdist
194+ run: |
195+ mkdir -p sdist
196+ $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist
197+ - name: unpack
198+ run: |
199+ mkdir -p unpacked
200+ find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \;
201+ - name: generate cabal.project
202+ run: |
203+ PKGDIR_postgresql_simple="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/postgresql-simple-[0-9.]*')"
204+ echo "PKGDIR_postgresql_simple=${PKGDIR_postgresql_simple}" >> "$GITHUB_ENV"
205+ rm -f cabal.project cabal.project.local
206+ touch cabal.project
207+ touch cabal.project.local
208+ echo "packages: ${PKGDIR_postgresql_simple}" >> cabal.project
209+ echo "package postgresql-simple" >> cabal.project
210+ echo " ghc-options: -Werror=missing-methods" >> cabal.project
211+ cat >> cabal.project <<EOF
212+ allow-newer: cryptohash-md5-0.11.100.1:base
213+ EOF
214+ $HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(postgresql-simple)$/; }' >> cabal.project.local
215+ cat cabal.project
216+ cat cabal.project.local
217+ - name: dump install plan
218+ run: |
219+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all
220+ cabal-plan
221+ - name: restore cache
222+ uses: actions/cache/restore@v3
223+ with:
224+ key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
225+ path: ~/.cabal/store
226+ restore-keys: ${{ runner.os }}-${{ matrix.compiler }}-
227+ - name: install dependencies
228+ run: |
229+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all
230+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all
231+ - name: build w/o tests
232+ run: |
233+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
234+ - name: build
235+ run: |
236+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always
237+ - name: tests
238+ run: |
239+ $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct
240+ - name: cabal check
241+ run: |
242+ cd ${PKGDIR_postgresql_simple} || false
243+ ${CABAL} -vnormal check
244+ - name: haddock
245+ run: |
246+ $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
247+ - name: unconstrained build
248+ run: |
249+ rm -f cabal.project.local
250+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
251+ - name: save cache
252+ uses: actions/cache/save@v3
253+ if: always()
254+ with:
255+ key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
256+ path: ~/.cabal/store
.github/workflows/simple.ymlnew file+58
@@ -0,0 +1,58 @@
1+name: Simple
2+on:
3+ push:
4+ branches:
5+ - master
6+ pull_request:
7+ branches:
8+ - master
9+
10+jobs:
11+ native:
12+ name: "Simple: GHC ${{ matrix.ghc }} on ${{ matrix.os }}"
13+ runs-on: ${{ matrix.os }}
14+ strategy:
15+ matrix:
16+ os: [macos-latest, windows-latest]
17+ ghc: ['8.10','9.0','9.2','9.4.8','9.6.3','9.8.1']
18+ fail-fast: false
19+ timeout-minutes:
20+ 60
21+ steps:
22+ - name: Set git to use LF
23+ run: |
24+ git config --global core.autocrlf false
25+ git config --global core.eol lf
26+
27+ - name: Set up Haskell
28+ id: setup-haskell
29+ uses: haskell/actions/setup@v2
30+ with:
31+ ghc-version: ${{ matrix.ghc }}
32+ cabal-version: '3.10.2.0'
33+
34+ - name: Set up PostgreSQL
35+ uses: ikalnytskyi/action-setup-postgres@v3
36+ id: postgres
37+ with:
38+ username: ci
39+ password: sw0rdfish
40+ database: test
41+
42+ - name: Checkout
43+ uses: actions/checkout@v3.0.2
44+
45+ - name: Cache
46+ uses: actions/cache@v2.1.3
47+ with:
48+ path: ${{ steps.setup-haskell.outputs.cabal-store }}
49+ key: ${{ runner.os }}-${{ matrix.ghc }}-${{ github.sha }}
50+ restore-keys: ${{ runner.os }}-${{ matrix.ghc }}-
51+
52+ - name: Build
53+ run: cabal build all --enable-tests
54+
55+ - name: Test
56+ run: cabal test all --enable-tests --test-show-details=direct
57+ env:
58+ DATABASE_CONNSTRING: ${{ steps.postgres.outputs.connection-uri }}
.gitignore+2
@@ -13,3 +13,5 @@ dist/
1313cabal.sandbox.config
1414/dist-newstyle
1515.ghc.environment.*
16+cabal.project.local
17+dump-core
.travis.ymldeleted−129
@@ -1,129 +0,0 @@
1−# This Travis job script has been generated by a script via
2−#
3−# runghc make_travis_yml_2.hs '--branch' 'master' '--output' '.travis.yml' 'postgresql-simple.cabal'
4−#
5−# For more information, see https://github.com/haskell-CI/haskell-ci
6−#
7−language: c
8−sudo: false
9−
10−services:
11− - postgresql
12−
13−git:
14− submodules: false # whether to recursively clone submodules
15−
16−branches:
17− only:
18− - master
19−
20−cache:
21− directories:
22− - $HOME/.cabal/packages
23− - $HOME/.cabal/store
24−
25−before_cache:
26− - rm -fv $HOME/.cabal/packages/hackage.haskell.org/build-reports.log
27− # remove files that are regenerated by 'cabal update'
28− - rm -fv $HOME/.cabal/packages/hackage.haskell.org/00-index.*
29− - rm -fv $HOME/.cabal/packages/hackage.haskell.org/*.json
30− - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.cache
31− - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar
32− - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar.idx
33−
34− - rm -rfv $HOME/.cabal/packages/head.hackage
35−
36−matrix:
37− include:
38− - compiler: "ghc-8.6.3"
39− # env: TEST=--disable-tests BENCH=--disable-benchmarks
40− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.6.3], sources: [hvr-ghc]}}
41− - compiler: "ghc-8.4.4"
42− # env: TEST=--disable-tests BENCH=--disable-benchmarks
43− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.4.4], sources: [hvr-ghc]}}
44− - compiler: "ghc-8.2.2"
45− # env: TEST=--disable-tests BENCH=--disable-benchmarks
46− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.2.2], sources: [hvr-ghc]}}
47− - compiler: "ghc-8.0.2"
48− # env: TEST=--disable-tests BENCH=--disable-benchmarks
49− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.0.2], sources: [hvr-ghc]}}
50− - compiler: "ghc-7.10.3"
51− # env: TEST=--disable-tests BENCH=--disable-benchmarks
52− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-7.10.3], sources: [hvr-ghc]}}
53− - compiler: "ghc-7.8.4"
54− # env: TEST=--disable-tests BENCH=--disable-benchmarks
55− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-7.8.4], sources: [hvr-ghc]}}
56− - compiler: "ghc-7.6.3"
57− # env: TEST=--disable-tests BENCH=--disable-benchmarks
58− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-7.6.3], sources: [hvr-ghc]}}
59−
60−before_install:
61− - HC=${CC}
62− - HCPKG=${HC/ghc/ghc-pkg}
63− - unset CC
64− - ROOTDIR=$(pwd)
65− - mkdir -p $HOME/.local/bin
66− - "PATH=/opt/ghc/bin:/opt/ghc-ppa-tools/bin:$HOME/local/bin:$PATH"
67− - HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1 * 10000 + \2 * 100 + \3/') ))
68− - echo $HCNUMVER
69−
70−install:
71− - cabal --version
72− - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
73− - BENCH=${BENCH---enable-benchmarks}
74− - TEST=${TEST---enable-tests}
75− - HADDOCK=${HADDOCK-true}
76− - UNCONSTRAINED=${UNCONSTRAINED-true}
77− - NOINSTALLEDCONSTRAINTS=${NOINSTALLEDCONSTRAINTS-false}
78− - GHCHEAD=${GHCHEAD-false}
79− - travis_retry cabal update -v
80− - "sed -i.bak 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config"
81− - rm -fv cabal.project cabal.project.local
82− - grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
83− - "printf 'packages: \".\"\\n' > cabal.project"
84− - "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
85− - touch cabal.project.local
86− - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- postgresql-simple | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
87− - cat cabal.project || true
88− - cat cabal.project.local || true
89− - if [ -f "./configure.ac" ]; then
90− (cd "." && autoreconf -i);
91− fi
92− - rm -f cabal.project.freeze
93− - cabal new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all
94− - cabal new-build -w ${HC} --disable-tests --disable-benchmarks --project-file="cabal.project" --dep -j2 all
95− - rm -rf .ghc.environment.* "."/dist
96− - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
97−
98−# Here starts the actual work to be performed for the package under test;
99−# any command which exits with a non-zero exit code causes the build to fail.
100−script:
101− # test that source-distributions can be generated
102− - cabal new-sdist all
103− - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
104− - cd ${DISTDIR} || false
105− - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
106− - "printf 'packages: postgresql-simple-*/*.cabal\\n' > cabal.project"
107− - "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
108− - touch cabal.project.local
109− - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- postgresql-simple | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
110− - cat cabal.project || true
111− - cat cabal.project.local || true
112− # this builds all libraries and executables (without tests/benchmarks)
113− - cabal new-build -w ${HC} --disable-tests --disable-benchmarks all
114−
115− # build & run tests, build benchmarks
116− - cabal new-build -w ${HC} ${TEST} ${BENCH} all
117− - if [ "x$TEST" = "x--enable-tests" ]; then cabal new-test -w ${HC} ${TEST} ${BENCH} all; fi
118−
119− # cabal check
120− - (cd postgresql-simple-* && cabal check)
121−
122− # haddock
123− - if $HADDOCK; then cabal new-haddock -w ${HC} ${TEST} ${BENCH} all; else echo "Skipping haddock generation";fi
124−
125− # Build without installed constraints for packages in global-db
126− - if $UNCONSTRAINED; then rm -f cabal.project.local; echo cabal new-build -w ${HC} --disable-tests --disable-benchmarks all; else echo "Not building without installed constraints"; fi
127−
128−# REGENDATA ["--branch","master","--output",".travis.yml","postgresql-simple.cabal"]
129−# EOF
CHANGES.md+82−7
@@ -1,6 +1,81 @@
1−### Version 0.6 (2018-09-25)
1+### Version 0.7.0.0 (2023-07-31)
22
3− * *Breaking change*: Use `Only` package's `Only for a common 1-tuple.
3+ * Remove Eq Null instance. (Future `base` may break it, we remove it profilacticly).
4+ * Use `postgresql-libpq >=0.10.0.0`. It includes critical fixes,
5+ by using `postgresql-simple >=0.7` you won't get older `postgresql-libpq`.
6+ * Drop support for GHC prior 8.6
7+ * Added a class for `postgresql-simple` exceptions: `SomePostgreSqlException`.
8+ This allows to catch all `postgresql-simple` exceptions at once.
9+ (c.f. `AsyncException` in `base).
10+
11+### Version 0.6.5.1 (2023-07-09)
12+
13+ * Support `aeson-2.2.0.0`
14+
15+### Version 0.6.5 (2022-10-30)
16+
17+ * Add `withConnect`
18+
19+### Version 0.6.4 (2021-01-06)
20+
21+ * Add foldCopyData helper function
22+ Thanks to Sebastián Estrella for the implementation
23+ https://github.com/haskellari/postgresql-simple/pull/56
24+ * Implement support for postgresql 'interval' type
25+ Thanks to Andre Marques Lee for the implementation
26+ https://github.com/haskellari/postgresql-simple/pull/60
27+ * Depend on `time-compat` to provide uniform `time` related interface.
28+
29+### Version 0.6.3 (2020-11-15)
30+
31+ * Add `fromFieldJSONByteString`
32+ Thanks to tomjaguarpaw for the implementation
33+ https://github.com/haskellari/postgresql-simple/pull/47
34+ * Add `attoFieldParser`
35+ Thanks to Victor Nawothnig for the implementation
36+ https://github.com/haskellari/postgresql-simple/pull/45
37+ * Add `Identity` and `Const` instance
38+ Thanks to Cary Robbins for the implementation
39+ https://github.com/haskellari/postgresql-simple/pull/46
40+ * Add `withTransactionModeRetry'`, a variant of `withTransactionModeRetry`
41+ for all exception types.
42+ Thanks to Elliot Cameron for the implementation
43+ https://github.com/haskellari/postgresql-simple/pull/42
44+ * Fix spurious aborts when retrying transactions
45+ Thanks to Elliot Cameron for the implementation
46+ https://github.com/haskellari/postgresql-simple/pull/34
47+ * Add `Database.PostgreSQL.Simple.Newtypes` module
48+ with `Aeson` newtype.
49+ https://github.com/haskellari/postgresql-simple/pull/55
50+
51+### Version 0.6.2 (2019-04-26)
52+
53+ * Define `MonadFail Ok`.
54+
55+### Version 0.6.1 (2019-03-04)
56+
57+ * Escape double '??' to a literal '?
58+ Thanks to Felix Paulusma for the implementation.
59+ https://github.com/phadej/postgresql-simple/pull/5
60+
61+ * Mention GHC Generics support in the documentation.
62+ Thanks to Gabriel Gonzalez for the implementation.
63+ https://github.com/phadej/postgresql-simple/pull/6
64+
65+ * Better error message for "Query resulted in a command response
66+ Thanks to Max Amanshauser for the implementation.
67+ https://github.com/phadej/postgresql-simple/pull/7
68+
69+ * fromJSONField: Include JSONPath on JSON parse errors
70+ Thanks to Simon Hengel for the implementation.
71+ https://github.com/phadej/postgresql-simple/pull/2
72+
73+ * No TH in implementation
74+ https://github.com/phadej/postgresql-simple/pull/4
75+
76+### Version 0.6 (2018-10-16)
77+
78+ * *Breaking change*: Use `Only` package's `Only` for a common 1-tuple.
479
580 Consider a downstream library depending already both on
681 `Only` and `postgresql-simple` package. This library my define
@@ -196,7 +271,7 @@
196271 support UTF8 except for Mule Internal Code, the Multilingual
197272 Extensions for Emacs. An exception should be raised upon
198273 connecting to a database by the backend if the backend cannot
199− accomodate this requirement.
274+ accommodate this requirement.
200275
201276 * Added `Eq` and `Typeable` instances for `Connection`.
202277
@@ -316,7 +391,7 @@
316391 * De-emphasized connect and ConnectInfo in favor of connectPostgreSQL.
317392
318393### Version 0.4.2.2 (2014-05-15)
319− * Fixed compatibility with scientific-0.3.*, thanks to Adam Bergmark
394+ * Fixed compatibility with scientific-0.3.\*, thanks to Adam Bergmark
320395
321396 * Improved documentation of the FromField module, as well as the fold,
322397 foldWithOptions, executeMany, and returning operators.
@@ -334,7 +409,7 @@
334409
335410 * Changed the Identifier and QualifiedIdentifier to use Text in
336411 order to avoid encoding errors. Technically this requires a
337− major verson bump, but let's pretend 0.4.1.0 didn't happen.
412+ major version bump, but let's pretend 0.4.1.0 didn't happen.
338413
339414 * Removed non-exhaustive cases in the ToField instance for Values,
340415 and tweaked error messages.
@@ -494,7 +569,7 @@
494569
495570 * Added a brand new TypeInfo system that gives FromField instances
496571 convenient and efficient access to the pg_type metatable. This
497− replaced the older typename cache, and was neccesary to properly
572+ replaced the older typename cache, and was necessary to properly
498573 support postgres array types. Thanks to Bas van Dijk for his
499574 work on this feature.
500575
@@ -586,7 +661,7 @@
586661 now the preferred way of dealing with `timestamp` (without time zone).
587662
588663 * `Database.PostgreSQL.Simple.Time` is a new module that offers types
589− that accomodate PostgreSQL's infinities.
664+ that accommodate PostgreSQL's infinities.
590665
591666 * All time-related `FromField`/`ToField` instances are now based on new,
592667 higher-speed parsers and printers instead of those provided by the
CONTRIBUTING.mdnew file+13
@@ -0,0 +1,13 @@
1+Patches welcome!
2+
3+- If you are only going to bump bounds:
4+ - If it's really **only bounds**, please simply open an issue (so you'll have a URL to refer to). I have a semi-automated process to make revisions, pull requests only disturb it.
5+ - If patch includes **source code change** (i.e. I'll need to make a release), and it's a patch to support **newer base/GHC version**:
6+ - Amend `tested-with` to include that GHC
7+ - Regenerate `.github/workflows/haskell-ci.yml` with `haskell-ci regenerate` (get the latest from [GitHub haskell-ci/haskell-ci](https://github.com/haskell-ci/haskell-ci))
8+
9+- Don't edit `CHANGELOG.md`, rather include a copyable entry in your pull request description. I often process pull requests in bulk, and everyone editing the `CHANGELOG.md` causes unnecessary conflicts.
10+- For the same reason, do not edit `version` or `x-revision`
11+
12+- I use [`stylish-haskell`](https://github.com/jaspervdj/stylish-haskell) to format imports. I encourage you to use it too, when contributing.
13+- General code style is 4 spaces, just look around how it looks, it's not so strict.
README.md−3
@@ -1,7 +1,4 @@
1−[![Continuous Integration status][status-png]][status]
21[![Hackage page (downloads and API reference)][hackage-png]][hackage]
32
4− [status-png]: https://api.travis-ci.org/lpsmith/postgresql-simple.svg
5− [status]: http://travis-ci.org/lpsmith/postgresql-simple?branch=master
63 [hackage-png]: http://img.shields.io/hackage/v/postgresql-simple.svg
74 [hackage]: http://hackage.haskell.org/package/postgresql-simple
cabal.haskell-cinew file+5
@@ -0,0 +1,5 @@
1+branches: master
2+postgresql: True
3+
4+-- services aren't supported on Travis on macOS
5+-- osx: 8.4.4
cabal.project+2
@@ -1,2 +1,4 @@
11packages: .
22tests: true
3+
4+allow-newer: cryptohash-md5-0.11.100.1:base
cabal.project.local.winnew file+4
@@ -0,0 +1,4 @@
1+constraints: these -assoc
2+
3+package postgresql-libpq
4+ flags: -use-pkg-config
postgresql-simple.cabal+129−146
@@ -1,192 +1,175 @@
1−Cabal-version: 1.12
2−Name: postgresql-simple
3−Version: 0.6
4−
5−Synopsis: Mid-Level PostgreSQL client library
6−Description:
7− Mid-Level PostgreSQL client library, forked from mysql-simple.
8−License: BSD3
9−License-file: LICENSE
10−Author: Bryan O'Sullivan, Leon P Smith
11−Maintainer: Oleg Grenrus <oleg.grenrus@iki.fi>
12−Copyright: (c) 2011 MailRank, Inc.
13− (c) 2011-2018 Leon P Smith
14− (c) 2018 Oleg Grenrus
15−Category: Database
16−Build-type: Simple
17−
1+cabal-version: 1.12
2+name: postgresql-simple
3+version: 0.7.0.0
4+x-revision: 1
5+synopsis: Mid-Level PostgreSQL client library
6+description:
7+ Mid-Level PostgreSQL client library, forked from mysql-simple.
8+
9+license: BSD3
10+license-file: LICENSE
11+author: Bryan O'Sullivan, Leon P Smith
12+maintainer: Oleg Grenrus <oleg.grenrus@iki.fi>
13+copyright:
14+ (c) 2011 MailRank, Inc.
15+ (c) 2011-2018 Leon P Smith
16+ (c) 2018-2020 Oleg Grenrus
17+
18+category: Database
19+build-type: Simple
1820extra-source-files:
19− CONTRIBUTORS
2021 CHANGES.md
22+ CONTRIBUTORS
2123 test/results/malformed-input.expected
2224 test/results/unique-constraint-violation.expected
2325
2426tested-with:
25− GHC == 7.6.3
26− || == 7.8.4
27− || == 7.10.3
28− || == 8.0.2
29− || == 8.2.2
30− || == 8.4.4
31− || == 8.6.3
32−
33−Library
34− default-language: Haskell2010
35− hs-source-dirs: src
36− Exposed-modules:
37− Database.PostgreSQL.Simple
38− Database.PostgreSQL.Simple.Arrays
39− Database.PostgreSQL.Simple.Copy
40− Database.PostgreSQL.Simple.Cursor
41− Database.PostgreSQL.Simple.FromField
42− Database.PostgreSQL.Simple.FromRow
43− Database.PostgreSQL.Simple.LargeObjects
44− Database.PostgreSQL.Simple.HStore
45− Database.PostgreSQL.Simple.HStore.Internal
46− Database.PostgreSQL.Simple.Notification
47− Database.PostgreSQL.Simple.Ok
48− Database.PostgreSQL.Simple.Range
49− Database.PostgreSQL.Simple.SqlQQ
50− Database.PostgreSQL.Simple.Time
51− Database.PostgreSQL.Simple.Time.Internal
52− Database.PostgreSQL.Simple.ToField
53− Database.PostgreSQL.Simple.ToRow
54− Database.PostgreSQL.Simple.Transaction
55− Database.PostgreSQL.Simple.TypeInfo
56− Database.PostgreSQL.Simple.TypeInfo.Macro
57− Database.PostgreSQL.Simple.TypeInfo.Static
58− Database.PostgreSQL.Simple.Types
59− Database.PostgreSQL.Simple.Errors
60− Database.PostgreSQL.Simple.Vector
61− Database.PostgreSQL.Simple.Vector.Unboxed
62−
63−-- Other-modules:
64− Database.PostgreSQL.Simple.Internal
65−
66− Other-modules:
67− Database.PostgreSQL.Simple.Compat
68− Database.PostgreSQL.Simple.HStore.Implementation
69− Database.PostgreSQL.Simple.Internal.PQResultUtils
70− Database.PostgreSQL.Simple.Time.Implementation
71− Database.PostgreSQL.Simple.Time.Internal.Parser
72− Database.PostgreSQL.Simple.Time.Internal.Printer
73− Database.PostgreSQL.Simple.TypeInfo.Types
27+ GHC ==8.6.5
28+ || ==8.8.4
29+ || ==8.10.7
30+ || ==9.0.2
31+ || ==9.2.8
32+ || ==9.4.8
33+ || ==9.6.3
34+ || ==9.8.1
35+
36+library
37+ default-language: Haskell2010
38+ hs-source-dirs: src
39+ exposed-modules:
40+ Database.PostgreSQL.Simple
41+ Database.PostgreSQL.Simple.Arrays
42+ Database.PostgreSQL.Simple.Copy
43+ Database.PostgreSQL.Simple.Cursor
44+ Database.PostgreSQL.Simple.Errors
45+ Database.PostgreSQL.Simple.FromField
46+ Database.PostgreSQL.Simple.FromRow
47+ Database.PostgreSQL.Simple.HStore
48+ Database.PostgreSQL.Simple.HStore.Internal
49+ Database.PostgreSQL.Simple.Internal
50+ Database.PostgreSQL.Simple.LargeObjects
51+ Database.PostgreSQL.Simple.Newtypes
52+ Database.PostgreSQL.Simple.Notification
53+ Database.PostgreSQL.Simple.Ok
54+ Database.PostgreSQL.Simple.Range
55+ Database.PostgreSQL.Simple.SqlQQ
56+ Database.PostgreSQL.Simple.Time
57+ Database.PostgreSQL.Simple.Time.Internal
58+ Database.PostgreSQL.Simple.ToField
59+ Database.PostgreSQL.Simple.ToRow
60+ Database.PostgreSQL.Simple.Transaction
61+ Database.PostgreSQL.Simple.TypeInfo
62+ Database.PostgreSQL.Simple.TypeInfo.Macro
63+ Database.PostgreSQL.Simple.TypeInfo.Static
64+ Database.PostgreSQL.Simple.Types
65+ Database.PostgreSQL.Simple.Vector
66+ Database.PostgreSQL.Simple.Vector.Unboxed
67+
68+ -- Other-modules:
69+ other-modules:
70+ Database.PostgreSQL.Simple.Compat
71+ Database.PostgreSQL.Simple.HStore.Implementation
72+ Database.PostgreSQL.Simple.Internal.PQResultUtils
73+ Database.PostgreSQL.Simple.Time.Implementation
74+ Database.PostgreSQL.Simple.Time.Internal.Parser
75+ Database.PostgreSQL.Simple.Time.Internal.Printer
76+ Database.PostgreSQL.Simple.TypeInfo.Types
7477
7578 -- GHC bundled libs
76− Build-depends:
77− base >=4.6.0.0 && <4.13
78− , bytestring >=0.10.0.0 && <0.11
79− , containers >=0.5.0.0 && <0.7
80− , time >=1.4.0.1 && <1.9
81− , transformers >=0.3.0.0 && <0.6
82− , template-haskell >=2.8.0.0 && <2.15
83− , text >=1.2.3.0 && <1.3
79+ build-depends:
80+ base >=4.12.0.0 && <4.20
81+ , bytestring >=0.10.8.2 && <0.13
82+ , containers >=0.6.0.1 && <0.7
83+ , template-haskell >=2.14.0.0 && <2.22
84+ , text >=1.2.3.0 && <1.3 || >=2.0 && <2.2
85+ , time-compat >=1.9.5 && <1.12
86+ , transformers >=0.5.6.2 && <0.7
8487
8588 -- Other dependencies
86− Build-depends:
87− aeson >=1.4.1.0 && <1.5
88− , attoparsec >=0.13.2.2 && <0.14
89− , bytestring-builder >=0.10.8.1.0 && <0.11
90− , case-insensitive >=1.2.0.11 && <1.3
91− , hashable >=1.2.7.0 && <1.3
92− , Only >=0.1 && <0.1.1
93− , postgresql-libpq >=0.9.4.2 && < 0.10
94− , uuid-types >=1.0.3 && <1.1
95− , scientific >=0.3.6.2 && <0.4
96− , vector >=0.12.0.1 && <0.13
97−
98− if !impl(ghc >= 8.0)
99− Build-depends:
100− semigroups >=0.18.5 && <0.19
101−
102− if !impl(ghc >= 7.6)
103− Build-depends:
104− ghc-prim
89+ build-depends:
90+ aeson >=2.1.2.1 && <2.3
91+ , attoparsec >=0.14.4 && <0.15
92+ , case-insensitive >=1.2.1.0 && <1.3
93+ , hashable >=1.4.3.0 && <1.5
94+ , Only >=0.1 && <0.1.1
95+ , postgresql-libpq >=0.10.0.0 && <0.11
96+ , scientific >=0.3.7.0 && <0.4
97+ , uuid-types >=1.0.5 && <1.1
98+ , vector >=0.13.0.0 && <0.14
10599
106100 default-extensions:
101+ BangPatterns
107102 DoAndIfThenElse
108103 OverloadedStrings
109− BangPatterns
110− ViewPatterns
111104 TypeOperators
105+ ViewPatterns
112106
113− ghc-options: -Wall -fno-warn-name-shadowing
107+ ghc-options: -Wall -fno-warn-name-shadowing
114108
115109source-repository head
116110 type: git
117− location: http://github.com/phadej/postgresql-simple
118−
119−source-repository this
120− type: git
121− location: http://github.com/phadej/postgresql-simple
122− tag: v0.6
111+ location: http://github.com/haskellari/postgresql-simple
123112
124113test-suite inspection
125− if !impl(ghc >= 8.0)
126− buildable: False
127−
128114 default-language: Haskell2010
129− type: exitcode-stdio-1.0
130− hs-source-dirs: test
131− main-is: Inspection.hs
115+ type: exitcode-stdio-1.0
116+ hs-source-dirs: test
117+ main-is: Inspection.hs
132118 build-depends:
133119 base
134− , inspection-testing >=0.4.1.1 && <0.5
135− , postgresql-simple
120+ , inspection-testing >=0.4.1.1 && <0.6
136121 , postgresql-libpq
122+ , postgresql-simple
137123 , tasty
138124 , tasty-hunit
139125
140126test-suite test
141− default-language: Haskell2010
142− type: exitcode-stdio-1.0
143−
144− hs-source-dirs: test
145− main-is: Main.hs
127+ default-language: Haskell2010
128+ type: exitcode-stdio-1.0
129+ hs-source-dirs: test
130+ main-is: Main.hs
146131 other-modules:
147132 Common
133+ Exception
134+ Interval
148135 Notify
149136 Serializable
150137 Time
151138
152− ghc-options: -Wall -fno-warn-name-shadowing -fno-warn-unused-do-bind
153−
139+ ghc-options: -threaded
140+ ghc-options: -Wall -fno-warn-name-shadowing -fno-warn-unused-do-bind
154141 default-extensions:
155142 NamedFieldPuns
156143 OverloadedStrings
144+ PatternGuards
157145 Rank2Types
158146 RecordWildCards
159− PatternGuards
160147 ScopedTypeVariables
161148
162− build-depends: base
163− , aeson
164− , base16-bytestring
165− , bytestring
166− , containers
167− , cryptohash
168− , filepath
169− , tasty
170− , tasty-hunit
171− , tasty-golden
172− , HUnit
173− , postgresql-simple
174− , text
175− , time
176− , vector
177− , case-insensitive
178−
179− if !impl(ghc >= 7.6)
180− build-depends:
181− ghc-prim
149+ build-depends:
150+ aeson
151+ , base
152+ , base16-bytestring
153+ , bytestring
154+ , case-insensitive
155+ , containers
156+ , cryptohash-md5 >=0.11.100.1 && <0.12
157+ , filepath
158+ , HUnit
159+ , postgresql-simple
160+ , tasty
161+ , tasty-golden
162+ , tasty-hunit
163+ , text
164+ , time-compat
165+ , vector
182166
183167benchmark select
184168 default-language: Haskell2010
185169 type: exitcode-stdio-1.0
186−
187− hs-source-dirs: bench
188− main-is: Select.hs
189−
190− build-depends: base
191− , postgresql-simple
192− , vector
170+ hs-source-dirs: bench
171+ main-is: Select.hs
172+ build-depends:
173+ base
174+ , postgresql-simple
175+ , vector
src/Database/PostgreSQL/Simple.hs+22−10
@@ -64,6 +64,7 @@ module Database.PostgreSQL.Simple
6464 , Only(..)
6565 , (:.)(..)
6666 -- ** Exceptions
67+ , SomePostgreSqlException(..)
6768 , SqlError(..)
6869 , PQ.ExecStatus(..)
6970 , FormatError(..)
@@ -73,6 +74,7 @@ module Database.PostgreSQL.Simple
7374 , Base.connectPostgreSQL
7475 , Base.close
7576 , Base.connect
77+ , Base.withConnect
7678 , Base.ConnectInfo(..)
7779 , Base.defaultConnectInfo
7880 , Base.postgreSQLConnectionString
@@ -276,21 +278,31 @@ parseTemplate template =
276278 skipSpace = B.dropWhile isSpace_ascii
277279
278280
279−
280281buildQuery :: Connection -> Query -> ByteString -> [Action] -> IO Builder
281282buildQuery conn q template xs =
282283 zipParams (split template) <$> mapM (buildAction conn q xs) xs
283284 where split s =
284− let (h,t) = B.break (=='?') s
285+ -- This part escapes double '??'s to make literal '?'s possible
286+ -- in PostgreSQL queries using the JSON operators: @?@, @?|@ and @?&@
287+ let (h,t) = breakOnSingleQuestionMark s
285288 in byteString h
286289 : if B.null t
287290 then []
288291 else split (B.tail t)
289292 zipParams (t:ts) (p:ps) = t <> p <> zipParams ts ps
290293 zipParams [t] [] = t
291− zipParams _ _ = fmtError (show (B.count '?' template) ++
292− " '?' characters, but " ++
294+ zipParams _ _ = fmtError (show countSingleQs ++
295+ " single '?' characters, but " ++
293296 show (length xs) ++ " parameters") q xs
297+ countSingleQs = go 0 template
298+ where go i "" = (i :: Int)
299+ go i bs = case qms of
300+ ("?","?") -> go i nextQMBS
301+ ("?",_) -> go (i+1) nextQMBS
302+ _ -> i
303+ where qms = B.splitAt 1 qmBS
304+ (qmBS,nextQMBS) = B.splitAt 2 qmBS'
305+ qmBS' = B.dropWhile (/= '?') bs
294306
295307-- | Execute an @INSERT@, @UPDATE@, or other SQL query that is not
296308-- expected to return results.
@@ -328,7 +340,7 @@ execute conn template qs = do
328340-- @
329341-- executeMany c [sql|
330342-- UPDATE sometable
331−-- SET sometable.y = upd.y
343+-- SET y = upd.y
332344-- FROM (VALUES (?,?)) as upd(x,y)
333345-- WHERE sometable.x = upd.x
334346-- |] [(1, \"hello\"),(2, \"world\")]
@@ -521,7 +533,7 @@ foldWithOptions_ :: (FromRow r) =>
521533 -> a -- ^ Initial state for result consumer.
522534 -> (a -> r -> IO a) -- ^ Result consumer.
523535 -> IO a
524−foldWithOptions_ opts conn query a f = doFold opts fromRow conn query query a f
536+foldWithOptions_ opts conn query' a f = doFold opts fromRow conn query' query' a f
525537
526538-- | A version of 'foldWithOptions_' taking a parser as an argument
527539foldWithOptionsAndParser_ :: FoldOptions
@@ -531,7 +543,7 @@ foldWithOptionsAndParser_ :: FoldOptions
531543 -> a -- ^ Initial state for result consumer.
532544 -> (a -> r -> IO a) -- ^ Result consumer.
533545 -> IO a
534−foldWithOptionsAndParser_ opts parser conn query a f = doFold opts parser conn query query a f
546+foldWithOptionsAndParser_ opts parser conn query' a f = doFold opts parser conn query' query' a f
535547
536548doFold :: FoldOptions
537549 -> RowParser row
@@ -567,8 +579,8 @@ doFold FoldOptions{..} parser conn _template q a0 f = do
567579 go = bracket declare closeCursor $ \cursor ->
568580 let loop a = fetch cursor a >>=
569581 \r -> case r of
570− Left a -> return a
571− Right a -> loop a
582+ Left x -> return x
583+ Right x -> loop x
572584 in loop a0
573585
574586-- FIXME: choose the Automatic chunkSize more intelligently
@@ -890,7 +902,7 @@ forEachWith_ parser conn template = foldWith_ parser conn template () . const
890902-- all values of the given PostgreSQL type is considered \"compatible\".
891903-- For instance, you can always extract a PostgreSQL 16-bit @SMALLINT@
892904-- column to a Haskell 'Int'. The Haskell 'Float' type can accurately
893−-- represent a @SMALLINT@, so it is considered compatble with those types.
905+-- represent a @SMALLINT@, so it is considered compatible with those types.
894906--
895907-- * A numeric compatibility check is based only on the type of a
896908-- column, /not/ on its values. For instance, a PostgreSQL 64-bit
src/Database/PostgreSQL/Simple/Arrays.hs+2−2
@@ -42,9 +42,9 @@ array delim = char '{' *> option [] (arrays <|> strings) <* char '}'
4242quoted :: Parser ByteString
4343quoted = char '"' *> option "" contents <* char '"'
4444 where
45− esc = char '\\' *> (char '\\' <|> char '"')
45+ esc' = char '\\' *> (char '\\' <|> char '"')
4646 unQ = takeWhile1 (notInClass "\"\\")
47− contents = mconcat <$> many (unQ <|> B.singleton <$> esc)
47+ contents = mconcat <$> many (unQ <|> B.singleton <$> esc')
4848
4949-- | Recognizes a plain string literal, not containing quotes or brackets and
5050-- not containing the delimiter character.
src/Database/PostgreSQL/Simple/Compat.hs−53
@@ -12,36 +12,13 @@ module Database.PostgreSQL.Simple.Compat
1212 ) where
1313
1414import qualified Control.Exception as E
15−import Data.Monoid
1615import Data.ByteString (ByteString)
17−#if MIN_VERSION_bytestring(0,10,0)
1816import Data.ByteString.Lazy (toStrict)
19−#else
20−import qualified Data.ByteString as B
21−import Data.ByteString.Lazy (toChunks)
22−#endif
2317import Data.ByteString.Builder (Builder, toLazyByteString)
24−
25−#if MIN_VERSION_scientific(0,3,0)
2618import Data.Text.Lazy.Builder.Scientific (scientificBuilder)
27−#else
28−import Data.Scientific (scientificBuilder)
29−#endif
30−
31−#if __GLASGOW_HASKELL__ >= 702
3219import System.IO.Unsafe (unsafeDupablePerformIO)
33−#elif __GLASGOW_HASKELL__ >= 611
34−import GHC.IO (unsafeDupablePerformIO)
35−#else
36−import GHC.IOBase (unsafeDupablePerformIO)
37−#endif
38−
3920import Data.Fixed (Pico)
40−#if MIN_VERSION_base(4,7,0)
4121import Data.Fixed (Fixed(MkFixed))
42−#else
43−import Unsafe.Coerce (unsafeCoerce)
44−#endif
4522
4623-- | Like 'E.mask', but backported to base before version 4.3.0.
4724--
@@ -51,44 +28,14 @@ import Unsafe.Coerce (unsafeCoerce)
5128-- 'withTransactionMode' function calls the restore callback only once, so we
5229-- don't need that polymorphism.
5330mask :: ((IO a -> IO a) -> IO b) -> IO b
54−#if MIN_VERSION_base(4,3,0)
5531mask io = E.mask $ \restore -> io restore
56−#else
57−mask io = do
58− b <- E.blocked
59− E.block $ io $ \m -> if b then m else E.unblock m
60−#endif
6132{-# INLINE mask #-}
6233
63−#if !MIN_VERSION_base(4,5,0)
64−infixr 6 <>
65−
66−(<>) :: Monoid m => m -> m -> m
67−(<>) = mappend
68−{-# INLINE (<>) #-}
69−#endif
70−
7134toByteString :: Builder -> ByteString
72−#if MIN_VERSION_bytestring(0,10,0)
7335toByteString x = toStrict (toLazyByteString x)
74−#else
75−toByteString x = B.concat (toChunks (toLazyByteString x))
76−#endif
77−
78−#if MIN_VERSION_base(4,7,0)
7936
8037toPico :: Integer -> Pico
8138toPico = MkFixed
8239
8340fromPico :: Pico -> Integer
8441fromPico (MkFixed i) = i
85−
86−#else
87−
88−toPico :: Integer -> Pico
89−toPico = unsafeCoerce
90−
91−fromPico :: Pico -> Integer
92−fromPico = unsafeCoerce
93−
94−#endif
src/Database/PostgreSQL/Simple/Copy.hs+27−6
@@ -32,6 +32,7 @@ module Database.PostgreSQL.Simple.Copy
3232 ( copy
3333 , copy_
3434 , CopyOutResult(..)
35+ , foldCopyData
3536 , getCopyData
3637 , putCopyData
3738 , putCopyEnd
@@ -48,13 +49,13 @@ import qualified Data.ByteString.Char8 as B
4849import qualified Database.PostgreSQL.LibPQ as PQ
4950import Database.PostgreSQL.Simple
5051import Database.PostgreSQL.Simple.Types
51−import Database.PostgreSQL.Simple.Internal
52+import Database.PostgreSQL.Simple.Internal hiding (result, row)
5253
5354
5455-- | Issue a @COPY FROM STDIN@ or @COPY TO STDOUT@ query. In the former
5556-- case, the connection's state will change to @CopyIn@; in the latter,
5657-- @CopyOut@. The connection must be in the ready state in order
57−-- to call this function. Performs parameter subsitution.
58+-- to call this function. Performs parameter substitution.
5859
5960copy :: ( ToRow params ) => Connection -> Query -> params -> IO ()
6061copy conn template qs = do
@@ -85,12 +86,8 @@ doCopy funcName conn template q = do
8586 PQ.TuplesOk -> err
8687 PQ.CopyOut -> return ()
8788 PQ.CopyIn -> return ()
88−#if MIN_VERSION_postgresql_libpq(0,9,3)
8989 PQ.CopyBoth -> errMsg "COPY BOTH is not supported"
90−#endif
91−#if MIN_VERSION_postgresql_libpq(0,9,2)
9290 PQ.SingleTuple -> errMsg "single-row mode is not supported"
93−#endif
9491 PQ.BadResponse -> throwResultError funcName result status
9592 PQ.NonfatalError -> throwResultError funcName result status
9693 PQ.FatalError -> throwResultError funcName result status
@@ -104,6 +101,30 @@ data CopyOutResult
104101 deriving (Eq, Typeable, Show)
105102
106103
104+-- | Fold over @COPY TO STDOUT@ query passing each copied row to an accumulator
105+-- and calling a post-process at the end. A connection must be in the
106+-- @CopyOut@ state in order to call this function.
107+--
108+-- __Example__
109+--
110+-- > (acc, count) <- foldCopyData conn
111+-- > (\acc row -> return (row:acc))
112+-- > (\acc count -> return (acc, count))
113+-- > []
114+
115+foldCopyData
116+ :: Connection -- ^ Database connection
117+ -> (a -> B.ByteString -> IO a) -- ^ Accumulate one row of the result
118+ -> (a -> Int64 -> IO b) -- ^ Post-process accumulator with a count of rows
119+ -> a -- ^ Initial accumulator
120+ -> IO b -- ^ Result
121+foldCopyData conn f g !acc = do
122+ result <- getCopyData conn
123+ case result of
124+ CopyOutRow row -> f acc row >>= foldCopyData conn f g
125+ CopyOutDone count -> g acc count
126+
127+
107128-- | Retrieve some data from a @COPY TO STDOUT@ query. A connection
108129-- must be in the @CopyOut@ state in order to call this function. If this
109130-- returns a 'CopyOutRow', the connection remains in the @CopyOut@ state,
src/Database/PostgreSQL/Simple/Cursor.hs+5−5
@@ -30,7 +30,7 @@ import Data.Monoid (mconcat)
3030import Database.PostgreSQL.Simple.Compat ((<>), toByteString)
3131import Database.PostgreSQL.Simple.FromRow (FromRow(..))
3232import Database.PostgreSQL.Simple.Types (Query(..))
33−import Database.PostgreSQL.Simple.Internal as Base
33+import Database.PostgreSQL.Simple.Internal as Base hiding (result, row)
3434import Database.PostgreSQL.Simple.Internal.PQResultUtils
3535import Database.PostgreSQL.Simple.Transaction
3636import qualified Database.PostgreSQL.LibPQ as PQ
@@ -90,9 +90,9 @@ foldForward cursor = foldForwardWithParser cursor fromRow
9090foldM' :: (Ord n, Num n) => (a -> n -> IO a) -> a -> n -> n -> IO a
9191foldM' f a lo hi = loop a lo
9292 where
93− loop a !n
94− | n > hi = return a
93+ loop x !n
94+ | n > hi = return x
9595 | otherwise = do
96− a' <- f a n
97− loop a' (n+1)
96+ x' <- f x n
97+ loop x' (n+1)
9898{-# INLINE foldM' #-}
src/Database/PostgreSQL/Simple/Errors.hs+4−3
@@ -25,7 +25,6 @@ module Database.PostgreSQL.Simple.Errors
2525 )
2626 where
2727
28−import Control.Applicative
2928import Control.Exception as E
3029
3130import Data.Attoparsec.ByteString.Char8
@@ -60,11 +59,13 @@ data ConstraintViolation
6059 deriving (Show, Eq, Ord, Typeable)
6160
6261-- Default instance should be enough
63−instance Exception ConstraintViolation
62+instance Exception ConstraintViolation where
63+ toException = postgresqlExceptionToException
64+ fromException = postgresqlExceptionFromException
6465
6566
6667-- | Tries to convert 'SqlError' to 'ConstrainViolation', checks sqlState and
67−-- succeedes only if able to parse sqlErrorMsg.
68+-- succeeds only if able to parse sqlErrorMsg.
6869--
6970-- > createUser = handleJust constraintViolation handler $ execute conn ...
7071-- > where
src/Database/PostgreSQL/Simple/FromField.hs+70−33
@@ -2,6 +2,7 @@
22{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
33{-# LANGUAGE PatternGuards, ScopedTypeVariables #-}
44{-# LANGUAGE RecordWildCards #-}
5+{-# LANGUAGE PolyKinds #-}
56
67{- |
78Module: Database.PostgreSQL.Simple.FromField
@@ -106,26 +107,27 @@ module Database.PostgreSQL.Simple.FromField
106107 , PQ.Oid(..)
107108 , PQ.Format(..)
108109 , pgArrayFieldParser
110+ , attoFieldParser
109111
110112 , optionalField
111113 , fromJSONField
114+ , fromFieldJSONByteString
112115 ) where
113116
114117#include "MachDeps.h"
115118
116−import Control.Applicative ( (<|>), (<$>), pure, (*>), (<*) )
119+import Control.Applicative ( Const(Const), (<|>), (<$>), pure, (*>), (<*) )
117120import Control.Concurrent.MVar (MVar, newMVar)
118−import Control.Exception (Exception)
121+import Control.Exception (Exception (toException, fromException))
119122import qualified Data.Aeson as JSON
120−import qualified Data.Aeson.Internal as JSON
121−import qualified Data.Aeson.Parser as JSON (value')
122123import Data.Attoparsec.ByteString.Char8 hiding (Result)
123124import Data.ByteString (ByteString)
124125import qualified Data.ByteString.Char8 as B
126+import Data.Functor.Identity (Identity(Identity))
125127import Data.Int (Int16, Int32, Int64)
126128import Data.IORef (IORef, newIORef)
127129import Data.Ratio (Ratio)
128−import Data.Time ( UTCTime, ZonedTime, LocalTime, Day, TimeOfDay )
130+import Data.Time.Compat ( UTCTime, ZonedTime, LocalTime, Day, TimeOfDay, CalendarDiffTime )
129131import Data.Typeable (Typeable, typeOf)
130132import Data.Vector (Vector)
131133import Data.Vector.Mutable (IOVector)
@@ -152,6 +154,8 @@ import qualified Data.UUID.Types as UUID
152154import Data.Scientific (Scientific)
153155import GHC.Real (infinity, notANumber)
154156
157+import qualified Data.Aeson.Types as JSON
158+
155159-- | Exception thrown if conversion from a SQL value to a Haskell
156160-- value fails.
157161data ResultError = Incompatible { errSQLType :: String
@@ -178,7 +182,9 @@ data ResultError = Incompatible { errSQLType :: String
178182 -- between metadata and actual data in a row).
179183 deriving (Eq, Show, Typeable)
180184
181−instance Exception ResultError
185+instance Exception ResultError where
186+ toException = postgresqlExceptionToException
187+ fromException = postgresqlExceptionFromException
182188
183189left :: Exception a => a -> Conversion b
184190left = conversionError
@@ -244,9 +250,11 @@ tableOid Field{..} = toMaybeOid (unsafeDupablePerformIO (PQ.ftable result column
244250 then Nothing
245251 else Just x
246252
247−-- | If the column has a table associated with it, this returns the number
248−-- off the associated table column. Numbering starts from 0. Analogous
249−-- to libpq's @PQftablecol@.
253+-- | If the column has a table associated with it, this returns the
254+-- number of the associated table column. Table columns have
255+-- nonzero numbers. Zero is returned if the specified column is not
256+-- a simple reference to a table column, or when using pre-3.0
257+-- protocol. Analogous to libpq's @PQftablecol@.
250258
251259tableColumn :: Field -> Int
252260tableColumn Field{..} = fromCol (unsafeDupablePerformIO (PQ.ftablecol result column))
@@ -265,6 +273,12 @@ instance FromField () where
265273 | typeOid f /= TI.voidOid = returnError Incompatible f ""
266274 | otherwise = pure ()
267275
276+instance (FromField a) => FromField (Const a b) where
277+ fromField f bs = Const <$> fromField f bs
278+
279+instance (FromField a) => FromField (Identity a) where
280+ fromField f bs = Identity <$> fromField f bs
281+
268282-- | For dealing with null values. Compatible with any postgresql type
269283-- compatible with type @a@. Note that the type is not checked if
270284-- the value is null, although it is inadvisable to rely on this
@@ -300,9 +314,9 @@ instance FromField Bool where
300314
301315-- | \"char\", bpchar
302316instance FromField Char where
303− fromField f bs =
317+ fromField f bs0 =
304318 if (eq TI.charOid \/ eq TI.bpcharOid) (typeOid f)
305− then case bs of
319+ then case bs0 of
306320 Nothing -> returnError UnexpectedNull f ""
307321 Just bs -> if B.length bs /= 1
308322 then returnError ConversionFailed f "length not 1"
@@ -311,11 +325,11 @@ instance FromField Char where
311325
312326-- | int2
313327instance FromField Int16 where
314− fromField = atto ok16 $ signed decimal
328+ fromField = attoFieldParser ok16 $ signed decimal
315329
316330-- | int2, int4
317331instance FromField Int32 where
318− fromField = atto ok32 $ signed decimal
332+ fromField = attoFieldParser ok32 $ signed decimal
319333
320334#if WORD_SIZE_IN_BITS < 64
321335-- | int2, int4, and if compiled as 64-bit code, int8 as well.
@@ -325,36 +339,36 @@ instance FromField Int32 where
325339-- This library was compiled as 64-bit code.
326340#endif
327341instance FromField Int where
328− fromField = atto okInt $ signed decimal
342+ fromField = attoFieldParser okInt $ signed decimal
329343
330344-- | int2, int4, int8
331345instance FromField Int64 where
332− fromField = atto ok64 $ signed decimal
346+ fromField = attoFieldParser ok64 $ signed decimal
333347
334348-- | int2, int4, int8
335349instance FromField Integer where
336− fromField = atto ok64 $ signed decimal
350+ fromField = attoFieldParser ok64 $ signed decimal
337351
338352-- | int2, float4 (Uses attoparsec's 'double' routine, for
339353-- better accuracy convert to 'Scientific' or 'Rational' first)
340354instance FromField Float where
341− fromField = atto ok (realToFrac <$> pg_double)
355+ fromField = attoFieldParser ok (realToFrac <$> pg_double)
342356 where ok = eq TI.float4Oid \/ eq TI.int2Oid
343357
344358-- | int2, int4, float4, float8 (Uses attoparsec's 'double' routine, for
345359-- better accuracy convert to 'Scientific' or 'Rational' first)
346360instance FromField Double where
347− fromField = atto ok pg_double
361+ fromField = attoFieldParser ok pg_double
348362 where ok = eq TI.float4Oid \/ eq TI.float8Oid \/ eq TI.int2Oid \/ eq TI.int4Oid
349363
350364-- | int2, int4, int8, float4, float8, numeric
351365instance FromField (Ratio Integer) where
352− fromField = atto ok pg_rational
366+ fromField = attoFieldParser ok pg_rational
353367 where ok = eq TI.float4Oid \/ eq TI.float8Oid \/ eq TI.int2Oid \/ eq TI.int4Oid \/ eq TI.int8Oid \/ eq TI.numericOid
354368
355369-- | int2, int4, int8, float4, float8, numeric
356370instance FromField Scientific where
357− fromField = atto ok rational
371+ fromField = attoFieldParser ok rational
358372 where ok = eq TI.float4Oid \/ eq TI.float8Oid \/ eq TI.int2Oid \/ eq TI.int4Oid \/ eq TI.int8Oid \/ eq TI.numericOid
359373
360374unBinary :: Binary t -> t
@@ -382,7 +396,7 @@ instance FromField SB.ByteString where
382396
383397-- | oid
384398instance FromField PQ.Oid where
385− fromField f dat = PQ.Oid <$> atto (== TI.oidOid) decimal f dat
399+ fromField f dat = PQ.Oid <$> attoFieldParser (== TI.oidOid) decimal f dat
386400
387401-- | bytea, name, text, \"char\", bpchar, varchar, unknown
388402instance FromField LB.ByteString where
@@ -390,7 +404,7 @@ instance FromField LB.ByteString where
390404
391405unescapeBytea :: Field -> SB.ByteString
392406 -> Conversion (Binary SB.ByteString)
393−unescapeBytea f str = case unsafeDupablePerformIO (PQ.unescapeBytea str) of
407+unescapeBytea f str' = case unsafeDupablePerformIO (PQ.unescapeBytea str') of
394408 Nothing -> returnError ConversionFailed f "unescapeBytea failed"
395409 Just str -> pure (Binary str)
396410
@@ -475,14 +489,23 @@ instance FromField LocalTimestamp where
475489instance FromField Date where
476490 fromField = ff TI.dateOid "Date" parseDate
477491
492+-- | interval. Requires you to configure intervalstyle as @iso_8601@.
493+--
494+-- You can configure intervalstyle on every connection with a @SET@ command,
495+-- but for better performance you may want to configure it permanently in the
496+-- file found with @SHOW config_file;@ .
497+--
498+instance FromField CalendarDiffTime where
499+ fromField = ff TI.intervalOid "CalendarDiffTime" parseCalendarDiffTime
500+
478501ff :: PQ.Oid -> String -> (B8.ByteString -> Either String a)
479502 -> Field -> Maybe B8.ByteString -> Conversion a
480−ff compatOid hsType parse f mstr =
503+ff compatOid hsType parseBS f mstr =
481504 if typeOid f /= compatOid
482505 then err Incompatible ""
483506 else case mstr of
484507 Nothing -> err UnexpectedNull ""
485− Just str -> case parse str of
508+ Just str -> case parseBS str of
486509 Left msg -> err ConversionFailed msg
487510 Right val -> return val
488511 where
@@ -520,10 +543,10 @@ pgArrayFieldParser fieldParser f mdat = do
520543 _ -> returnError Incompatible f ""
521544
522545fromArray :: FieldParser a -> TypeInfo -> Field -> Parser (Conversion [a])
523−fromArray fieldParser typeInfo f = sequence . (parseIt <$>) <$> array delim
546+fromArray fieldParser typInfo f = sequence . (parseIt <$>) <$> array delim
524547 where
525− delim = typdelim (typelem typeInfo)
526− fElem = f{ typeOid = typoid (typelem typeInfo) }
548+ delim = typdelim (typelem typInfo)
549+ fElem = f{ typeOid = typoid (typelem typInfo) }
527550
528551 parseIt item =
529552 fieldParser f' $ if item == Arrays.Plain "NULL" then Nothing else Just item'
@@ -552,12 +575,14 @@ instance FromField UUID where
552575
553576-- | json, jsonb
554577instance FromField JSON.Value where
555− fromField f mbs = parse =<< fromFieldJSONByteString f mbs
556− where parse bs = case parseOnly (JSON.value' <* endOfInput) bs of
578+ fromField f mbs = parseBS =<< fromFieldJSONByteString f mbs
579+ where parseBS bs = case JSON.eitherDecodeStrict' bs of
557580 Left err -> returnError ConversionFailed f err
558581 Right val -> pure val
559582
560583-- | Return the JSON ByteString directly
584+--
585+-- @since 0.6.3
561586fromFieldJSONByteString :: Field -> Maybe ByteString -> Conversion ByteString
562587fromFieldJSONByteString f mbs =
563588 if typeOid f /= TI.jsonOid && typeOid f /= TI.jsonbOid
@@ -659,10 +684,22 @@ returnError mkErr f msg = do
659684 (show (typeOf (undefined :: a)))
660685 msg
661686
662−atto :: forall a. (Typeable a)
663− => Compat -> Parser a -> Field -> Maybe ByteString
664− -> Conversion a
665−atto types p0 f dat = doFromField f types (go p0) dat
687+-- | Construct a field parser from an attoparsec parser. An 'Incompatible' error is thrown if the
688+-- PostgreSQL oid does not match the specified predicate.
689+--
690+-- @
691+-- instance FromField Int16 where
692+-- fromField = attoFieldParser ok16 (signed decimal)
693+-- @
694+--
695+-- @since 0.6.3
696+attoFieldParser :: forall a. (Typeable a)
697+ => (PQ.Oid -> Bool)
698+ -- ^ Predicate for whether the postgresql type oid is compatible with this parser
699+ -> Parser a
700+ -- ^ An attoparsec parser.
701+ -> FieldParser a
702+attoFieldParser types p0 f dat = doFromField f types (go p0) dat
666703 where
667704 go :: Parser a -> ByteString -> Conversion a
668705 go p s =
src/Database/PostgreSQL/Simple/FromRow.hs+2−2
@@ -131,8 +131,8 @@ fieldWith fieldP = RP $ do
131131 else do
132132 let !result = rowresult
133133 !typeOid = unsafeDupablePerformIO (PQ.ftype result column)
134− !field = Field{..}
135− lift (lift (fieldP field (getvalue result row column)))
134+ !field' = Field{..}
135+ lift (lift (fieldP field' (getvalue result row column)))
136136
137137field :: FromField a => RowParser a
138138field = fieldWith fromField
src/Database/PostgreSQL/Simple/HStore/Implementation.hs+3−9
@@ -22,9 +22,6 @@ import Data.ByteString.Builder (Builder, byteString, char8)
2222import qualified Data.ByteString.Builder as BU
2323import Data.ByteString.Internal (c2w, w2c)
2424import qualified Data.ByteString.Lazy as BL
25−#if !MIN_VERSION_bytestring(0,10,0)
26−import qualified Data.ByteString.Lazy.Internal as BL (foldrChunks)
27−#endif
2825import Data.Map(Map)
2926import qualified Data.Map as Map
3027import Data.Text(Text)
@@ -53,12 +50,12 @@ instance ToHStore HStoreBuilder where
5350toBuilder :: HStoreBuilder -> Builder
5451toBuilder x = case x of
5552 Empty -> mempty
56− Comma x -> x
53+ Comma c -> c
5754
5855toLazyByteString :: HStoreBuilder -> BL.ByteString
5956toLazyByteString x = case x of
6057 Empty -> BL.empty
61− Comma x -> BU.toLazyByteString x
58+ Comma c -> BU.toLazyByteString c
6259
6360instance Semigroup HStoreBuilder where
6461 Empty <> x = x
@@ -69,9 +66,6 @@ instance Semigroup HStoreBuilder where
6966
7067instance Monoid HStoreBuilder where
7168 mempty = Empty
72−#if !(MIN_VERSION_base(4,11,0))
73− mappend = (<>)
74−#endif
7569
7670class ToHStoreText a where
7771 toHStoreText :: a -> HStoreText
@@ -150,7 +144,7 @@ newtype HStoreMap = HStoreMap {fromHStoreMap :: Map Text Text} deriving (Eq, Or
150144
151145instance ToHStore HStoreMap where
152146 toHStore (HStoreMap xs) = Map.foldrWithKey f mempty xs
153− where f k v xs = hstore k v `mappend` xs
147+ where f k v xs' = hstore k v `mappend` xs'
154148
155149instance ToField HStoreMap where
156150 toField xs = toField (toHStore xs)
src/Database/PostgreSQL/Simple/Internal.hs+59−13
@@ -1,6 +1,8 @@
11{-# LANGUAGE CPP, BangPatterns, DoAndIfThenElse, RecordWildCards #-}
22{-# LANGUAGE DeriveDataTypeable, DeriveGeneric #-}
33{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE ExistentialQuantification #-}
5+{-# LANGUAGE InstanceSigs #-}
46
57------------------------------------------------------------------------------
68-- |
@@ -34,7 +36,6 @@ import Data.Int (Int64)
3436import qualified Data.IntMap as IntMap
3537import Data.IORef
3638import Data.Maybe(fromMaybe)
37−import Data.Monoid
3839import Data.String
3940import qualified Data.Text as T
4041import qualified Data.Text.Encoding as TE
@@ -82,6 +83,25 @@ data Connection = Connection {
8283instance Eq Connection where
8384 x == y = connectionHandle x == connectionHandle y
8485
86+-- | Superclass for postgresql exceptions
87+data SomePostgreSqlException = forall e. Exception e => SomePostgreSqlException e
88+ deriving Typeable
89+
90+postgresqlExceptionToException :: Exception e => e -> SomeException
91+postgresqlExceptionToException = toException . SomePostgreSqlException
92+
93+postgresqlExceptionFromException :: Exception e => SomeException -> Maybe e
94+postgresqlExceptionFromException x = do
95+ SomePostgreSqlException a <- fromException x
96+ cast a
97+
98+instance Show SomePostgreSqlException where
99+ showsPrec :: Int -> SomePostgreSqlException -> ShowS
100+ showsPrec p (SomePostgreSqlException e) = showsPrec p e
101+
102+instance Exception SomePostgreSqlException where
103+ displayException (SomePostgreSqlException e) = displayException e
104+
85105data SqlError = SqlError {
86106 sqlState :: ByteString
87107 , sqlExecStatus :: ExecStatus
@@ -93,7 +113,10 @@ data SqlError = SqlError {
93113fatalError :: ByteString -> SqlError
94114fatalError msg = SqlError "" FatalError msg "" ""
95115
96−instance Exception SqlError
116+instance Exception SqlError where
117+ toException = postgresqlExceptionToException
118+ fromException = postgresqlExceptionFromException
119+
97120
98121-- | Exception thrown if 'query' is used to perform an @INSERT@-like
99122-- operation, or 'execute' is used to perform a @SELECT@-like operation.
@@ -102,7 +125,9 @@ data QueryError = QueryError {
102125 , qeQuery :: Query
103126 } deriving (Eq, Show, Typeable)
104127
105−instance Exception QueryError
128+instance Exception QueryError where
129+ toException = postgresqlExceptionToException
130+ fromException = postgresqlExceptionFromException
106131
107132-- | Exception thrown if a 'Query' could not be formatted correctly.
108133-- This may occur if the number of \'@?@\' characters in the query
@@ -113,7 +138,9 @@ data FormatError = FormatError {
113138 , fmtParams :: [ByteString]
114139 } deriving (Eq, Show, Typeable)
115140
116−instance Exception FormatError
141+instance Exception FormatError where
142+ toException = postgresqlExceptionToException
143+ fromException = postgresqlExceptionFromException
117144
118145data ConnectInfo = ConnectInfo {
119146 connectHost :: String
@@ -155,6 +182,12 @@ defaultConnectInfo = ConnectInfo {
155182connect :: ConnectInfo -> IO Connection
156183connect = connectPostgreSQL . postgreSQLConnectionString
157184
185+-- | Memory bracket around 'connect' and 'close'.
186+--
187+-- @since 0.6.5
188+withConnect :: ConnectInfo -> (Connection -> IO c) -> IO c
189+withConnect connInfo = bracket (connect connInfo) close
190+
158191-- | Attempt to make a connection based on a libpq connection string.
159192-- See <https://www.postgresql.org/docs/9.5/static/libpq-connect.html#LIBPQ-CONNSTRING>
160193-- for more information. Also note that environment variables also affect
@@ -290,7 +323,7 @@ postgreSQLConnectionString connectInfo = fromString connstr
290323
291324 str name field
292325 | null value = id
293− | otherwise = showString name . quote value . space
326+ | otherwise = showString name . addQuotes value . space
294327 where value = field connectInfo
295328
296329 num name field
@@ -298,7 +331,7 @@ postgreSQLConnectionString connectInfo = fromString connstr
298331 | otherwise = showString name . shows value . space
299332 where value = field connectInfo
300333
301− quote str rest = '\'' : foldr delta ('\'' : rest) str
334+ addQuotes s rest = '\'' : foldr delta ('\'' : rest) s
302335 where
303336 delta c cs = case c of
304337 '\\' -> '\\' : '\\' : cs
@@ -386,7 +419,7 @@ finishExecute _conn q result = do
386419 nstr <- PQ.cmdTuples result
387420 return $ case nstr of
388421 Nothing -> 0 -- is this appropriate?
389− Just str -> toInteger str
422+ Just str -> mkInteger str
390423 PQ.TuplesOk -> do
391424 ncols <- PQ.nfields result
392425 throwIO $ QueryError ("execute resulted in " ++ show ncols ++
@@ -399,7 +432,7 @@ finishExecute _conn q result = do
399432 PQ.NonfatalError -> throwResultError "execute" result status
400433 PQ.FatalError -> throwResultError "execute" result status
401434 where
402− toInteger str = B8.foldl' delta 0 str
435+ mkInteger str = B8.foldl' delta 0 str
403436 where
404437 delta acc c =
405438 if '0' <= c && c <= '9'
@@ -414,8 +447,8 @@ throwResultError _ result status = do
414447 PQ.resultErrorField result PQ.DiagMessageDetail
415448 hint <- fromMaybe "" <$>
416449 PQ.resultErrorField result PQ.DiagMessageHint
417− state <- maybe "" id <$> PQ.resultErrorField result PQ.DiagSqlstate
418− throwIO $ SqlError { sqlState = state
450+ state' <- maybe "" id <$> PQ.resultErrorField result PQ.DiagSqlstate
451+ throwIO $ SqlError { sqlState = state'
419452 , sqlExecStatus = status
420453 , sqlErrorMsg = errormsg
421454 , sqlErrorDetail = detail
@@ -484,9 +517,6 @@ instance Alternative Conversion where
484517 Errors _ -> (oka <|>) <$> runConversion mb conn
485518
486519instance Monad Conversion where
487−#if !(MIN_VERSION_base(4,8,0))
488− return = pure
489−#endif
490520 m >>= f = Conversion $ \conn -> do
491521 oka <- runConversion m conn
492522 case oka of
@@ -589,3 +619,19 @@ escapeIdentifier = escapeWrap PQ.escapeIdentifier
589619
590620escapeByteaConn :: Connection -> ByteString -> IO (Either ByteString ByteString)
591621escapeByteaConn = escapeWrap PQ.escapeByteaConn
622+
623+breakOnSingleQuestionMark :: ByteString -> (ByteString, ByteString)
624+breakOnSingleQuestionMark b = go (B8.empty, b)
625+ where go (x,bs) = (x `B8.append` x',bs')
626+ -- seperate from first QM
627+ where tup@(noQ, restWithQ) = B8.break (=='?') bs
628+ -- if end of query, just return
629+ -- else check for second QM in 'go2'
630+ (x', bs') = maybe tup go2 $
631+ -- drop found QM and peek at next char
632+ B8.uncons restWithQ >>= B8.uncons . snd
633+ -- another QM after the first means:
634+ -- take literal QM and keep going.
635+ go2 ('?', t2) = go (noQ `B8.snoc` '?',t2)
636+ -- Anything else means
637+ go2 _ = tup
src/Database/PostgreSQL/Simple/Internal/PQResultUtils.hs+2−6
@@ -25,7 +25,7 @@ import Data.Foldable (for_)
2525import Database.PostgreSQL.Simple.FromField (ResultError(..))
2626import Database.PostgreSQL.Simple.Ok
2727import Database.PostgreSQL.Simple.Types (Query(..))
28−import Database.PostgreSQL.Simple.Internal as Base
28+import Database.PostgreSQL.Simple.Internal as Base hiding (result, row)
2929import Database.PostgreSQL.Simple.TypeInfo
3030import qualified Database.PostgreSQL.LibPQ as PQ
3131import qualified Data.ByteString.Char8 as B
@@ -73,15 +73,11 @@ finishQueryWith' q result k = do
7373 case status of
7474 PQ.TuplesOk -> k
7575 PQ.EmptyQuery -> queryErr "query: Empty query"
76− PQ.CommandOk -> queryErr "query resulted in a command response"
76+ PQ.CommandOk -> queryErr "query resulted in a command response (did you mean to use `execute` or forget a RETURNING?)"
7777 PQ.CopyOut -> queryErr "query: COPY TO is not supported"
7878 PQ.CopyIn -> queryErr "query: COPY FROM is not supported"
79−#if MIN_VERSION_postgresql_libpq(0,9,3)
8079 PQ.CopyBoth -> queryErr "query: COPY BOTH is not supported"
81−#endif
82−#if MIN_VERSION_postgresql_libpq(0,9,2)
8380 PQ.SingleTuple -> queryErr "query: single-row mode is not supported"
84−#endif
8581 PQ.BadResponse -> throwResultError "query" result status
8682 PQ.NonfatalError -> throwResultError "query" result status
8783 PQ.FatalError -> throwResultError "query" result status
src/Database/PostgreSQL/Simple/Newtypes.hsnew file+49
@@ -0,0 +1,49 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+-- | Module with newtypes suitable to usage with @DerivingVia@ or standalone.
4+--
5+-- The newtypes are named after packages they wrap.
6+module Database.PostgreSQL.Simple.Newtypes (
7+ Aeson (..), getAeson,
8+) where
9+
10+import Data.Typeable (Typeable)
11+import Database.PostgreSQL.Simple.ToField (ToField (..))
12+import Database.PostgreSQL.Simple.FromField (FromField (..), fromJSONField)
13+
14+import qualified Data.Aeson as Aeson
15+
16+-------------------------------------------------------------------------------
17+-- aeson
18+-------------------------------------------------------------------------------
19+
20+-- | A newtype wrapper with 'ToField' and 'FromField' instances
21+-- based on 'Aeson.ToJSON' and 'Aeson.FromJSON' type classes from @aeson@.
22+--
23+-- Example using @DerivingVia@:
24+--
25+-- @
26+-- data Foo = Foo Int String
27+-- deriving stock (Eq, Show, Generic) -- GHC built int
28+-- deriving anyclass ('Aeson.FromJSON', 'Aeson.ToJSON') -- Derived using GHC Generics
29+-- deriving ('ToField', 'FromField') via 'Aeson' Foo -- DerivingVia
30+-- @
31+--
32+-- Example using 'Aeson' newtype directly, for more ad-hoc queries
33+--
34+-- @
35+-- execute conn "INSERT INTO tbl (fld) VALUES (?)" (Only ('Aeson' x))
36+-- @
37+--
38+-- @since 0.6.3
39+newtype Aeson a = Aeson a
40+ deriving (Eq, Show, Read, Typeable, Functor)
41+
42+getAeson :: Aeson a -> a
43+getAeson (Aeson a) = a
44+
45+instance Aeson.ToJSON a => ToField (Aeson a) where
46+ toField = toField . Aeson.encode . getAeson
47+
48+instance (Aeson.FromJSON a, Typeable a) => FromField (Aeson a) where
49+ fromField f bs = fmap Aeson (fromJSONField f bs)
src/Database/PostgreSQL/Simple/Notification.hs−17
@@ -47,8 +47,6 @@ import GHC.IO.Exception ( ioe_location )
4747
4848#if defined(mingw32_HOST_OS)
4949import Control.Concurrent ( threadDelay )
50−#elif !MIN_VERSION_base(4,7,0)
51−import Control.Concurrent ( threadWaitRead )
5250#else
5351import GHC.Conc ( atomically )
5452import Control.Concurrent ( threadWaitReadSTM )
@@ -94,21 +92,6 @@ getNotification conn = join $ withConnection conn fetch
9492 -- with async exceptions, whereas threadDelay can.
9593 Just _fd -> do
9694 return (threadDelay 1000000 >> loop)
97−#elif !MIN_VERSION_base(4,7,0)
98− -- Technically there's a race condition that is usually benign.
99− -- If the connection is closed or reset after we drop the
100− -- lock, and then the fd index is reallocated to a new
101− -- descriptor before we call threadWaitRead, then
102− -- we could end up waiting on the wrong descriptor.
103− --
104− -- Now, if the descriptor becomes readable promptly, then
105− -- it's no big deal as we'll wake up and notice the change
106− -- on the next iteration of the loop. But if are very
107− -- unlucky, then we could end up waiting a long time.
108− Just fd -> do
109− return $ do
110− threadWaitRead fd `catch` (throwIO . setIOErrorLocation)
111− loop
11295#else
11396 -- This case fixes the race condition above. By registering
11497 -- our interest in the descriptor before we drop the lock,
src/Database/PostgreSQL/Simple/Ok.hs+7−4
@@ -37,6 +37,8 @@ import Control.Exception
3737import Control.Monad(MonadPlus(..))
3838import Data.Typeable
3939
40+import qualified Control.Monad.Fail as Fail
41+
4042-- FIXME: [SomeException] should probably be something else, maybe
4143-- a difference list (or a tree?)
4244
@@ -70,13 +72,14 @@ instance MonadPlus Ok where
7072 mplus = (<|>)
7173
7274instance Monad Ok where
73−#if !(MIN_VERSION_base(4,8,0))
74− return = pure
75−#endif
76−
7775 Errors es >>= _ = Errors es
7876 Ok a >>= f = f a
7977
78+#if !(MIN_VERSION_base(4,13,0))
79+ fail = Fail.fail
80+#endif
81+
82+instance Fail.MonadFail Ok where
8083 fail str = Errors [SomeException (ErrorCall str)]
8184
8285-- | a way to reify a list of exceptions into a single exception
src/Database/PostgreSQL/Simple/Range.hs+7−7
@@ -38,7 +38,7 @@ import Data.Monoid (mempty)
3838import Data.Scientific (Scientific)
3939import qualified Data.Text.Lazy.Builder as LT
4040import qualified Data.Text.Lazy.Encoding as LT
41−import Data.Time (Day, LocalTime,
41+import Data.Time.Compat (Day, LocalTime,
4242 NominalDiffTime,
4343 TimeOfDay, UTCTime,
4444 ZonedTime,
@@ -110,19 +110,19 @@ containsBy cmp rng x =
110110 PGRange _lb NegInfinity -> False
111111 PGRange lb ub -> checkLB lb x && checkUB ub x
112112 where
113− checkLB lb x =
113+ checkLB lb y =
114114 case lb of
115115 NegInfinity -> True
116116 PosInfinity -> False
117− Inclusive a -> cmp a x /= GT
118− Exclusive a -> cmp a x == LT
117+ Inclusive a -> cmp a y /= GT
118+ Exclusive a -> cmp a y == LT
119119
120− checkUB ub x =
120+ checkUB ub y =
121121 case ub of
122122 NegInfinity -> False
123123 PosInfinity -> True
124− Inclusive z -> cmp x z /= GT
125− Exclusive z -> cmp x z == LT
124+ Inclusive z -> cmp y z /= GT
125+ Exclusive z -> cmp y z == LT
126126
127127lowerBound :: Parser (a -> RangeBound a)
128128lowerBound = (A.char '(' *> pure Exclusive) <|> (A.char '[' *> pure Inclusive)
src/Database/PostgreSQL/Simple/SqlQQ.hs+1−1
@@ -35,7 +35,7 @@ import Data.String
3535-- > (beginTime,endTime,string)
3636--
3737-- This quasiquoter returns a literal string expression of type 'Query',
38−-- and attempts to mimimize whitespace; otherwise the above query would
38+-- and attempts to minimize whitespace; otherwise the above query would
3939-- consist of approximately half whitespace when sent to the database
4040-- backend. It also recognizes and strips out standard sql comments "--".
4141--
src/Database/PostgreSQL/Simple/Time.hs+2
@@ -227,6 +227,7 @@ module Database.PostgreSQL.Simple.Time
227227 , parseUTCTimestamp
228228 , parseZonedTimestamp
229229 , parseLocalTimestamp
230+ , parseCalendarDiffTime
230231 , dayToBuilder
231232 , utcTimeToBuilder
232233 , zonedTimeToBuilder
@@ -239,6 +240,7 @@ module Database.PostgreSQL.Simple.Time
239240 , localTimestampToBuilder
240241 , unboundedToBuilder
241242 , nominalDiffTimeToBuilder
243+ , calendarDiffTimeToBuilder
242244 ) where
243245
244246import Database.PostgreSQL.Simple.Time.Implementation
src/Database/PostgreSQL/Simple/Time/Implementation.hs+11−1
@@ -18,7 +18,8 @@ import Data.ByteString.Builder.Prim(primBounded)
1818import Control.Arrow((***))
1919import Control.Applicative
2020import qualified Data.ByteString as B
21−import Data.Time hiding (getTimeZone, getZonedTime)
21+import Data.Time.Compat (LocalTime, UTCTime, ZonedTime, Day, TimeOfDay, TimeZone, NominalDiffTime, utc)
22+import Data.Time.LocalTime.Compat (CalendarDiffTime)
2223import Data.Typeable
2324import Data.Maybe (fromMaybe)
2425import qualified Data.Attoparsec.ByteString.Char8 as A
@@ -77,6 +78,9 @@ parseLocalTimestamp = A.parseOnly (getLocalTimestamp <* A.endOfInput)
7778parseDate :: B.ByteString -> Either String Date
7879parseDate = A.parseOnly (getDate <* A.endOfInput)
7980
81+parseCalendarDiffTime :: B.ByteString -> Either String CalendarDiffTime
82+parseCalendarDiffTime = A.parseOnly (getCalendarDiffTime <* A.endOfInput)
83+
8084getUnbounded :: A.Parser a -> A.Parser (Unbounded a)
8185getUnbounded getFinite
8286 = (pure NegInfinity <* A.string "-infinity")
@@ -125,6 +129,9 @@ getUTCTime = TP.utcTime
125129getUTCTimestamp :: A.Parser UTCTimestamp
126130getUTCTimestamp = getUnbounded getUTCTime
127131
132+getCalendarDiffTime :: A.Parser CalendarDiffTime
133+getCalendarDiffTime = TP.calendarDiffTime
134+
128135dayToBuilder :: Day -> Builder
129136dayToBuilder = primBounded TPP.day
130137
@@ -164,3 +171,6 @@ dateToBuilder = unboundedToBuilder dayToBuilder
164171
165172nominalDiffTimeToBuilder :: NominalDiffTime -> Builder
166173nominalDiffTimeToBuilder = TPP.nominalDiffTime
174+
175+calendarDiffTimeToBuilder :: CalendarDiffTime -> Builder
176+calendarDiffTimeToBuilder = TPP.calendarDiffTime
src/Database/PostgreSQL/Simple/Time/Internal/Parser.hs+12−3
@@ -21,20 +21,24 @@ module Database.PostgreSQL.Simple.Time.Internal.Parser
2121 , localToUTCTimeOfDayHMS
2222 , utcTime
2323 , zonedTime
24+ , calendarDiffTime
2425 ) where
2526
2627import Control.Applicative ((<$>), (<*>), (<*), (*>))
2728import Database.PostgreSQL.Simple.Compat (toPico)
2829import Data.Attoparsec.ByteString.Char8 as A
2930import Data.Bits ((.&.))
31+import Data.ByteString (ByteString)
3032import Data.Char (ord)
3133import Data.Fixed (Pico)
3234import Data.Int (Int64)
3335import Data.Maybe (fromMaybe)
34−import Data.Time.Calendar (Day, fromGregorianValid, addDays)
35−import Data.Time.Clock (UTCTime(..))
36+import Data.Time.Calendar.Compat (Day, fromGregorianValid, addDays)
37+import Data.Time.Clock.Compat (UTCTime(..))
38+import Data.Time.Format.ISO8601.Compat (iso8601ParseM)
39+import Data.Time.LocalTime.Compat (CalendarDiffTime)
3640import qualified Data.ByteString.Char8 as B8
37−import qualified Data.Time.LocalTime as Local
41+import qualified Data.Time.LocalTime.Compat as Local
3842
3943-- | Parse a date of the form @YYYY-MM-DD@.
4044day :: Parser Day
@@ -193,3 +197,8 @@ zonedTime = Local.ZonedTime <$> localTime <*> (fromMaybe utc <$> timeZone)
193197
194198utc :: Local.TimeZone
195199utc = Local.TimeZone 0 False ""
200+
201+calendarDiffTime :: Parser CalendarDiffTime
202+calendarDiffTime = do
203+ contents <- takeByteString
204+ iso8601ParseM $ B8.unpack contents
src/Database/PostgreSQL/Simple/Time/Internal/Printer.hs+14−2
@@ -17,19 +17,23 @@ module Database.PostgreSQL.Simple.Time.Internal.Printer
1717 , localTime
1818 , zonedTime
1919 , nominalDiffTime
20+ , calendarDiffTime
2021 ) where
2122
2223import Control.Arrow ((>>>))
23−import Data.ByteString.Builder (Builder, integerDec)
24+import Data.ByteString.Builder (Builder, byteString, integerDec)
2425import Data.ByteString.Builder.Prim
2526 ( liftFixedToBounded, (>$<), (>*<)
2627 , BoundedPrim, primBounded, condB, emptyB, FixedPrim, char8, int32Dec)
2728import Data.Char ( chr )
2829import Data.Int ( Int32, Int64 )
29−import Data.Time
30+import Data.String (fromString)
31+import Data.Time.Compat
3032 ( UTCTime(..), ZonedTime(..), LocalTime(..), NominalDiffTime
3133 , Day, toGregorian, TimeOfDay(..), timeToTimeOfDay
3234 , TimeZone, timeZoneMinutes )
35+import Data.Time.Format.ISO8601.Compat (iso8601Show)
36+import Data.Time.LocalTime.Compat (CalendarDiffTime)
3337import Database.PostgreSQL.Simple.Compat ((<>), fromPico)
3438import Unsafe.Coerce (unsafeCoerce)
3539
@@ -121,3 +125,11 @@ nominalDiffTime :: NominalDiffTime -> Builder
121125nominalDiffTime xy = integerDec x <> primBounded frac (abs (fromIntegral y))
122126 where
123127 (x,y) = fromPico (unsafeCoerce xy) `quotRem` 1000000000000
128+
129+calendarDiffTime :: CalendarDiffTime -> Builder
130+calendarDiffTime = byteString
131+ . fromString
132+ -- from the docs: "Beware: fromString truncates multi-byte characters to octets".
133+ -- However, I think this is a safe usage, because ISO8601-encoding seems restricted
134+ -- to ASCII output.
135+ . iso8601Show
src/Database/PostgreSQL/Simple/ToField.hs+22−11
@@ -1,5 +1,6 @@
11{-# LANGUAGE CPP, DeriveDataTypeable, DeriveFunctor #-}
22{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
3+{-# LANGUAGE PolyKinds #-}
34
45------------------------------------------------------------------------------
56-- |
@@ -22,6 +23,7 @@ module Database.PostgreSQL.Simple.ToField
2223 , inQuotes
2324 ) where
2425
26+import Control.Applicative (Const(Const))
2527import qualified Data.Aeson as JSON
2628import Data.ByteString (ByteString)
2729import Data.ByteString.Builder
@@ -30,10 +32,12 @@ import Data.ByteString.Builder
3032 , wordDec, word8Dec, word16Dec, word32Dec, word64Dec
3133 , floatDec, doubleDec
3234 )
35+import Data.Functor.Identity (Identity(Identity))
3336import Data.Int (Int8, Int16, Int32, Int64)
3437import Data.List (intersperse)
3538import Data.Monoid (mappend)
36−import Data.Time (Day, TimeOfDay, LocalTime, UTCTime, ZonedTime, NominalDiffTime)
39+import Data.Time.Compat (Day, TimeOfDay, LocalTime, UTCTime, ZonedTime, NominalDiffTime)
40+import Data.Time.LocalTime.Compat (CalendarDiffTime)
3741import Data.Typeable (Typeable)
3842import Data.Word (Word, Word8, Word16, Word32, Word64)
3943import {-# SOURCE #-} Database.PostgreSQL.Simple.ToRow
@@ -55,11 +59,7 @@ import qualified Data.Vector as V
5559import qualified Database.PostgreSQL.LibPQ as PQ
5660import Database.PostgreSQL.Simple.Time
5761import Data.Scientific (Scientific)
58−#if MIN_VERSION_scientific(0,3,0)
5962import Data.Text.Lazy.Builder.Scientific (scientificBuilder)
60−#else
61−import Data.Scientific (scientificBuilder)
62−#endif
6363import Foreign.C.Types (CUInt(..))
6464
6565-- | How to render an element when substituting it into a query.
@@ -100,6 +100,14 @@ instance ToField Action where
100100 toField a = a
101101 {-# INLINE toField #-}
102102
103+instance (ToField a) => ToField (Const a b) where
104+ toField (Const a) = toField a
105+ {-# INLINE toField #-}
106+
107+instance (ToField a) => ToField (Identity a) where
108+ toField (Identity a) = toField a
109+ {-# INLINE toField #-}
110+
103111instance (ToField a) => ToField (Maybe a) where
104112 toField Nothing = renderNull
105113 toField (Just a) = toField a
@@ -282,6 +290,10 @@ instance ToField NominalDiffTime where
282290 toField = Plain . inQuotes . nominalDiffTimeToBuilder
283291 {-# INLINE toField #-}
284292
293+instance ToField CalendarDiffTime where
294+ toField = Plain . inQuotes . calendarDiffTimeToBuilder
295+ {-# INLINE toField #-}
296+
285297instance (ToField a) => ToField (PGArray a) where
286298 toField pgArray =
287299 case fromPGArray pgArray of
@@ -302,14 +314,13 @@ instance ToField UUID where
302314instance ToField JSON.Value where
303315 toField = toField . JSON.encode
304316
305−-- | Convert a Haskell value to a JSON 'JSON.Value' using
306−-- 'JSON.toJSON' and convert that to a field using 'toField'.
317+-- | Convert a Haskell value to JSON using 'JSON.toEncoding'.
307318--
308319-- This can be used as the default implementation for the 'toField'
309320-- method for Haskell types that have a JSON representation in
310321-- PostgreSQL.
311322toJSONField :: JSON.ToJSON a => a -> Action
312−toJSONField = toField . JSON.toJSON
323+toJSONField = toField . JSON.encode
313324
314325-- | Surround a string with single-quote characters: \"@'@\"
315326--
@@ -319,7 +330,7 @@ inQuotes b = quote `mappend` b `mappend` quote
319330 where quote = char8 '\''
320331
321332interleaveFoldr :: (a -> [b] -> [b]) -> b -> [b] -> [a] -> [b]
322−interleaveFoldr f b bs as = foldr (\a bs -> b : f a bs) bs as
333+interleaveFoldr f b bs' as = foldr (\a bs -> b : f a bs) bs' as
323334{-# INLINE interleaveFoldr #-}
324335
325336instance ToRow a => ToField (Values a) where
@@ -366,8 +377,8 @@ instance ToRow a => ToField (Values a) where
366377
367378 typedRows :: ToRow a => [a] -> [QualifiedIdentifier] -> [Action] -> [Action]
368379 typedRows [] _ _ = error funcname
369− typedRows (val:vals) types rest =
370− typedRow (toRow val) types (multiRows vals rest)
380+ typedRows (val:vals) typs rest =
381+ typedRow (toRow val) typs (multiRows vals rest)
371382
372383 untypedRows :: ToRow a => [a] -> [Action] -> [Action]
373384 untypedRows [] _ = error funcname
src/Database/PostgreSQL/Simple/Transaction.hs+16−12
@@ -17,6 +17,7 @@ module Database.PostgreSQL.Simple.Transaction
1717 , withTransactionLevel
1818 , withTransactionMode
1919 , withTransactionModeRetry
20+ , withTransactionModeRetry'
2021 , withTransactionSerializable
2122 , TransactionMode(..)
2223 , IsolationLevel(..)
@@ -146,31 +147,34 @@ withTransactionMode mode conn act =
146147 commit conn
147148 return r
148149
150+-- | 'withTransactionModeRetry'' but with the exception type pinned to 'SqlError'.
151+withTransactionModeRetry :: TransactionMode -> (SqlError -> Bool) -> Connection -> IO a -> IO a
152+withTransactionModeRetry = withTransactionModeRetry'
153+
149154-- | Like 'withTransactionMode', but also takes a custom callback to
150−-- determine if a transaction should be retried if an 'SqlError' occurs.
151−-- If the callback returns True, then the transaction will be retried.
152−-- If the callback returns False, or an exception other than an 'SqlError'
155+-- determine if a transaction should be retried if an exception occurs.
156+-- If the callback returns 'True', then the transaction will be retried.
157+-- If the callback returns 'False', or an exception other than an @e@
153158-- occurs then the transaction will be rolled back and the exception rethrown.
154159--
155160-- This is used to implement 'withTransactionSerializable'.
156−withTransactionModeRetry :: TransactionMode -> (SqlError -> Bool) -> Connection -> IO a -> IO a
157−withTransactionModeRetry mode shouldRetry conn act =
161+withTransactionModeRetry' :: forall a e. E.Exception e => TransactionMode -> (e -> Bool) -> Connection -> IO a -> IO a
162+withTransactionModeRetry' mode shouldRetry conn act =
158163 mask $ \restore ->
159164 retryLoop $ E.try $ do
160− a <- restore act
165+ a <- restore act `E.onException` rollback_ conn
161166 commit conn
162167 return a
163168 where
164− retryLoop :: IO (Either E.SomeException a) -> IO a
169+ retryLoop :: IO (Either e a) -> IO a
165170 retryLoop act' = do
166171 beginMode mode conn
167172 r <- act'
168173 case r of
169− Left e -> do
170− rollback_ conn
171− case fmap shouldRetry (E.fromException e) of
172− Just True -> retryLoop act'
173− _ -> E.throwIO e
174+ Left e ->
175+ case shouldRetry e of
176+ True -> retryLoop act'
177+ False -> E.throwIO e
174178 Right a ->
175179 return a
176180
src/Database/PostgreSQL/Simple/TypeInfo.hs+9−9
@@ -50,21 +50,21 @@ import Database.PostgreSQL.Simple.TypeInfo.Static
5050-- in the connections's cache.
5151
5252getTypeInfo :: Connection -> PQ.Oid -> IO TypeInfo
53−getTypeInfo conn@Connection{..} oid =
54− case staticTypeInfo oid of
55− Just name -> return name
56− Nothing -> modifyMVar connectionObjects $ getTypeInfo' conn oid
53+getTypeInfo conn@Connection{..} oid' =
54+ case staticTypeInfo oid' of
55+ Just name' -> return name'
56+ Nothing -> modifyMVar connectionObjects $ getTypeInfo' conn oid'
5757
5858getTypeInfo' :: Connection -> PQ.Oid -> TypeInfoCache
5959 -> IO (TypeInfoCache, TypeInfo)
60−getTypeInfo' conn oid oidmap =
61− case IntMap.lookup (oid2int oid) oidmap of
60+getTypeInfo' conn oid' oidmap =
61+ case IntMap.lookup (oid2int oid') oidmap of
6262 Just typeinfo -> return (oidmap, typeinfo)
6363 Nothing -> do
6464 names <- query conn "SELECT oid, typcategory, typdelim, typname,\
6565 \ typelem, typrelid\
6666 \ FROM pg_type WHERE oid = ?"
67− (Only oid)
67+ (Only oid')
6868 (oidmap', typeInfo) <-
6969 case names of
7070 [] -> return $ throw (fatalError "invalid type oid")
@@ -78,7 +78,7 @@ getTypeInfo' conn oid oidmap =
7878 rngsubtypeOids <- query conn "SELECT rngsubtype\
7979 \ FROM pg_range\
8080 \ WHERE rngtypid = ?"
81− (Only oid)
81+ (Only oid')
8282 case rngsubtypeOids of
8383 [Only rngsubtype_] -> do
8484 (oidmap', rngsubtype) <-
@@ -104,7 +104,7 @@ getTypeInfo' conn oid oidmap =
104104 _ -> fail "typename query returned more than one result"
105105 -- oid is a primary key, so the query should
106106 -- never return more than one result
107− let !oidmap'' = IntMap.insert (oid2int oid) typeInfo oidmap'
107+ let !oidmap'' = IntMap.insert (oid2int oid') typeInfo oidmap'
108108 return $! (oidmap'', typeInfo)
109109
110110getAttInfos :: Connection -> [(B.ByteString, PQ.Oid)] -> TypeInfoCache
src/Database/PostgreSQL/Simple/Types.hs−7
@@ -50,10 +50,6 @@ import Database.PostgreSQL.Simple.Compat (toByteString)
5050data Null = Null
5151 deriving (Read, Show, Typeable)
5252
53−instance Eq Null where
54− _ == _ = False
55− _ /= _ = False
56−
5753-- | A placeholder for the PostgreSQL @DEFAULT@ value.
5854data Default = Default
5955 deriving (Read, Show, Typeable)
@@ -97,9 +93,6 @@ instance Semigroup Query where
9793
9894instance Monoid Query where
9995 mempty = Query B.empty
100−#if !(MIN_VERSION_base(4,11,0))
101− mappend = (<>)
102−#endif
10396
10497-- | Wrap a list of values for use in an @IN@ clause. Replaces a
10598-- single \"@?@\" character with a parenthesized list of rendered
test/Exception.hsnew file+70
@@ -0,0 +1,70 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+
5+module Exception (testExceptions) where
6+
7+import Database.PostgreSQL.Simple
8+import Test.Tasty.HUnit (Assertion, assertBool)
9+import Common (TestEnv)
10+import Control.Exception (Exception (..), SomeException)
11+import Data.Maybe (isJust)
12+import Data.Either (isLeft)
13+import Control.Exception (throwIO, try)
14+
15+testExceptions :: TestEnv -> Assertion
16+testExceptions _ = do
17+ let sqlError = SqlError
18+ { sqlState = ""
19+ , sqlExecStatus = FatalError
20+ , sqlErrorMsg = ""
21+ , sqlErrorDetail = ""
22+ , sqlErrorHint = ""
23+ }
24+ let sqlEx :: SomeException = toException sqlError
25+ assertBool "SqlError is SomePostgreSqlException" $ isJust (fromException sqlEx :: Maybe SomePostgreSqlException)
26+ assertBool "SqlError is SqlError" $ isJust (fromException sqlEx :: Maybe SqlError)
27+ eSqlError :: Either SqlError () <- try $ throwIO sqlEx
28+ assertBool "Can catch SqlError" $ isLeft eSqlError
29+ eSqlPostgreSqlEx :: Either SomePostgreSqlException () <- try $ throwIO sqlEx
30+ assertBool "Can catch SomePostgreSqlException from SqlError" $ isLeft eSqlPostgreSqlEx
31+
32+ let formatError = FormatError
33+ { fmtMessage = ""
34+ , fmtQuery = ""
35+ , fmtParams = []
36+ }
37+ let formatEx :: SomeException = toException formatError
38+ assertBool "FormatError is SomePostgreSqlException" $ isJust (fromException formatEx :: Maybe SomePostgreSqlException)
39+ assertBool "FormatError is FormatError" $ isJust (fromException formatEx :: Maybe FormatError)
40+ eFormatError :: Either FormatError () <- try $ throwIO formatEx
41+ assertBool "Can catch FormatError" $ isLeft eFormatError
42+ eFormatPostreSqlEx :: Either SomePostgreSqlException () <- try $ throwIO formatEx
43+ assertBool "Can catch SomePostgreSqlException from FormatError" $ isLeft eFormatPostreSqlEx
44+
45+ let queryError = QueryError
46+ { qeMessage = ""
47+ , qeQuery = ""
48+ }
49+ let queryEx :: SomeException = toException queryError
50+ assertBool "QueryError is SomePostgreSqlException" $ isJust (fromException queryEx :: Maybe SomePostgreSqlException)
51+ assertBool "QueryError is QueryError" $ isJust (fromException queryEx :: Maybe QueryError)
52+ eQueryError :: Either QueryError () <- try $ throwIO queryEx
53+ assertBool "Can catch QueryError" $ isLeft eQueryError
54+ eQueryPostgreSqlEx :: Either SomePostgreSqlException () <- try $ throwIO queryEx
55+ assertBool "Can catch SomePostgreSqlException from QueryError" $ isLeft eQueryPostgreSqlEx
56+
57+ let resultError = Incompatible
58+ { errSQLType = ""
59+ , errSQLTableOid = Nothing
60+ , errSQLField = ""
61+ , errHaskellType = ""
62+ , errMessage = ""
63+ }
64+ let resultEx :: SomeException = toException resultError
65+ assertBool "ResultError is SomePostgreSqlException" $ isJust (fromException resultEx :: Maybe SomePostgreSqlException)
66+ assertBool "ResultError is ResultError" $ isJust (fromException resultEx :: Maybe ResultError)
67+ eResultEx :: Either ResultError () <- try $ throwIO resultEx
68+ assertBool "Can catch ResultError" $ isLeft eResultEx
69+ eResultPostgreSqlEx :: Either SomePostgreSqlException () <- try $ throwIO resultEx
70+ assertBool "Can catch SomePostgreSqlException from ResultError" $ isLeft eResultPostgreSqlEx
test/Inspection.hs+6−1
@@ -53,7 +53,12 @@ inspectionTests = testGroup "inspection"
5353
5454 -- byteaOid isn't inlined?
5555 , testCase "inlineTypoid" $
56− assertFailure' $(inspectTest $ 'lhs02 ==- 'rhs02)
56+#if __GLASGOW_HASKELL__ >= 808
57+ assertSuccess
58+#else
59+ assertFailure'
60+#endif
61+ $(inspectTest $ 'lhs02 ==- 'rhs02)
5762 ]
5863
5964assertSuccess :: Result -> IO ()
test/Interval.hsnew file+176
@@ -0,0 +1,176 @@
1+{-# LANGUAGE QuasiQuotes #-}
2+
3+{-
4+
5+Testing strategies:
6+
7+fromString . toString == id ** Todo?
8+
9+toString . fromString == almost id ** Todo?
10+
11+postgresql -> haskell -> postgresql * Done
12+
13+haskell -> postgresql -> haskell ** Todo?
14+
15+But still, what we really want to establish is that the two values
16+correspond; for example, a conversion that consistently added hour
17+when printed to a string and subtracted an hour when parsed from string
18+would still pass these tests.
19+
20+
21+Right now, we are checking that 1400+ timestamps in the range of 1860 to
22+2060 round trip from postgresql to haskell and back in 5 different timezones.
23+In addition to UTC, the four timezones were selected so that 2 have a positive
24+offset, and 2 have a negative offset, and that 2 have an offset of a
25+whole number of hours, while the other two do not.
26+
27+It may be worth adding a few more timezones to ensure better test coverage.
28+
29+We are checking a handful of selected timestamps to ensure we hit
30+various corner-cases in the code, in addition to 1400 timestamps randomly
31+generated with granularity of seconds down to microseconds in powers of ten.
32+
33+-}
34+
35+module Interval (testInterval) where
36+
37+import Common
38+import Control.Monad(forM_, replicateM_)
39+import Data.Time.Compat
40+import Data.Time.LocalTime.Compat (CalendarDiffTime(..))
41+import Data.ByteString(ByteString)
42+import Database.PostgreSQL.Simple.SqlQQ
43+
44+data IntervalTestCase = IntervalTestCase
45+ { label :: String
46+ , inputMonths :: Integer
47+ , inputSeconds :: NominalDiffTime
48+ , asText :: String
49+ }
50+ deriving (Eq, Show)
51+
52+testInterval :: TestEnv -> Assertion
53+testInterval env@TestEnv{..} = do
54+
55+ initializeTable env
56+
57+ let milliseconds = 0.001
58+ seconds = 1
59+ minutes = 60 * seconds
60+ hours = 60 * minutes
61+ days = 24 * hours
62+ weeks = 7 * days
63+ months = 1
64+ years = 12 * months
65+
66+ mapM (checkRoundTrip env)
67+ [ IntervalTestCase
68+ { label = "zero"
69+ , inputMonths = 0
70+ , inputSeconds = 0
71+ , asText = "PT0"
72+ }
73+ , IntervalTestCase
74+ { label = "1 year"
75+ , inputMonths = 1 * years
76+ , inputSeconds = 0
77+ , asText = "P1Y"
78+ }
79+ , IntervalTestCase
80+ { label = "2 months"
81+ , inputMonths = 2 * months
82+ , inputSeconds = 0
83+ , asText = "P2M"
84+ }
85+ , IntervalTestCase
86+ { label = "3 weeks"
87+ , inputMonths = 0
88+ , inputSeconds = 3 * weeks
89+ , asText = "P3W"
90+ }
91+ , IntervalTestCase
92+ { label = "4 days"
93+ , inputMonths = 0
94+ , inputSeconds = 4 * days
95+ , asText = "P4D"
96+ }
97+ , IntervalTestCase
98+ { label = "5 hours"
99+ , inputMonths = 0
100+ , inputSeconds = 5 * hours
101+ , asText = "PT5H"
102+ }
103+ , IntervalTestCase
104+ { label = "6 minutes"
105+ , inputMonths = 0
106+ , inputSeconds = 6 * minutes
107+ , asText = "PT6M"
108+ }
109+ , IntervalTestCase
110+ { label = "7 seconds"
111+ , inputMonths = 0
112+ , inputSeconds = 7 * seconds
113+ , asText = "PT7S"
114+ }
115+ , IntervalTestCase
116+ { label = "8 milliseconds"
117+ , inputMonths = 0
118+ , inputSeconds = 8 * milliseconds
119+ , asText = "PT0.008S"
120+ }
121+ , IntervalTestCase
122+ { label = "combination of intervals (day-size or bigger)"
123+ , inputMonths = 2 * years + 4 * months
124+ , inputSeconds = 3 * weeks + 5 * days
125+ , asText = "P2Y4M3W5D"
126+ }
127+ , IntervalTestCase
128+ { label = "combination of intervals (smaller than day-size)"
129+ , inputMonths = 0
130+ , inputSeconds = 18 * hours + 56 * minutes + 23 * seconds + 563 * milliseconds
131+ , asText = "PT18H56M23.563S"
132+ }
133+ , IntervalTestCase
134+ { label = "full combination of intervals"
135+ , inputMonths = 2 * years + 4 * months
136+ , inputSeconds = 3 * weeks + 5 * days + 18 * hours + 56 * minutes + 23 * seconds + 563 * milliseconds
137+ , asText = "P2Y4M3W5DT18H56M23.563S"
138+ }
139+ ]
140+
141+ return ()
142+
143+initializeTable :: TestEnv -> IO ()
144+initializeTable TestEnv{..} = withTransaction conn $ do
145+ execute_ conn
146+ [sql| CREATE TEMPORARY TABLE testinterval
147+ ( id serial, sample interval, PRIMARY KEY(id) ) |]
148+
149+ return ()
150+
151+checkRoundTrip :: TestEnv -> IntervalTestCase -> IO ()
152+checkRoundTrip TestEnv{..} IntervalTestCase{..} = do
153+
154+ let input = CalendarDiffTime
155+ { ctMonths = inputMonths
156+ , ctTime = inputSeconds
157+ }
158+
159+ [(returnedId :: Int, output :: CalendarDiffTime)] <- query conn [sql|
160+ INSERT INTO testinterval (sample)
161+ VALUES (?)
162+ RETURNING id, sample
163+ |] (Only input)
164+
165+ assertBool ("CalendarDiffTime did not round-trip from Haskell to SQL and back (" ++ label ++ ")") $
166+ output == input
167+
168+ [(Only isExpectedIso)] <- query conn [sql|
169+ SELECT sample = (?)::interval
170+ FROM testinterval
171+ WHERE id = ?
172+ |] (asText, returnedId)
173+
174+ assertBool ("CalendarDiffTime inserted did not match ISO8601 equivalent \"" ++ asText ++ "\". (" ++ label ++ ")")
175+ isExpectedIso
176+
test/Main.hs+179−32
@@ -3,25 +3,39 @@
33{-# LANGUAGE DeriveDataTypeable #-}
44{-# LANGUAGE DoAndIfThenElse #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+#if __GLASGOW_HASKELL__ >= 806
7+{-# LANGUAGE DerivingStrategies #-}
8+{-# LANGUAGE DerivingVia #-}
9+{-# LANGUAGE DeriveAnyClass #-}
10+#endif
11+module Main (main) where
12+
613import Common
14+import Database.PostgreSQL.Simple.Copy
15+import Database.PostgreSQL.Simple.ToField (ToField)
716import Database.PostgreSQL.Simple.FromField (FromField)
8−import Database.PostgreSQL.Simple.Types(Query(..),Values(..))
917import Database.PostgreSQL.Simple.HStore
10−import Database.PostgreSQL.Simple.Copy
18+import Database.PostgreSQL.Simple.Newtypes
19+import Database.PostgreSQL.Simple.Internal (breakOnSingleQuestionMark)
20+import Database.PostgreSQL.Simple.Types(Query(..),Values(..), PGArray(..))
1121import qualified Database.PostgreSQL.Simple.Transaction as ST
1222
1323import Control.Applicative
1424import Control.Exception as E
1525import Control.Monad
1626import Data.Char
17−import Data.List (sort)
27+import Data.Foldable (toList)
28+import Data.List (concat, sort)
1829import Data.IORef
30+import Data.Monoid ((<>))
31+import Data.String (fromString)
1932import Data.Typeable
2033import GHC.Generics (Generic)
2134
2235import Data.Aeson
2336import Data.ByteString (ByteString)
2437import qualified Data.ByteString as B
38+import qualified Data.ByteString.Char8 as BS8
2539import qualified Data.ByteString.Lazy.Char8 as BL
2640import Data.CaseInsensitive (CI)
2741import qualified Data.CaseInsensitive as CI
@@ -32,38 +46,46 @@ import qualified Data.Text.Encoding as T
3246import qualified Data.Vector as V
3347import System.FilePath
3448import System.Timeout(timeout)
35−import Data.Time(getCurrentTime, diffUTCTime)
49+import Data.Time.Compat (getCurrentTime, diffUTCTime)
50+import System.Environment (getEnvironment)
3651
3752import Test.Tasty
3853import Test.Tasty.Golden
3954import Notify
4055import Serializable
4156import Time
57+import Interval
58+import Exception (testExceptions)
4259
4360tests :: TestEnv -> TestTree
4461tests env = testGroup "tests"
4562 $ map ($ env)
4663 [ testBytea
47− , testCase "ExecuteMany" . testExecuteMany
48− , testCase "Fold" . testFold
49− , testCase "Notify" . testNotify
50− , testCase "Serializable" . testSerializable
51− , testCase "Time" . testTime
52− , testCase "Array" . testArray
53− , testCase "Array of nullables" . testNullableArray
54− , testCase "HStore" . testHStore
55− , testCase "citext" . testCIText
56− , testCase "JSON" . testJSON
57− , testCase "Savepoint" . testSavepoint
58− , testCase "Unicode" . testUnicode
59− , testCase "Values" . testValues
60− , testCase "Copy" . testCopy
64+ , testCase "ExecuteMany" . testExecuteMany
65+ , testCase "Fold" . testFold
66+ , testCase "Notify" . testNotify
67+ , testCase "Serializable" . testSerializable
68+ , testCase "Time" . testTime
69+ , testCase "Interval" . testInterval
70+ , testCase "Array" . testArray
71+ , testCase "Array of nullables" . testNullableArray
72+ , testCase "HStore" . testHStore
73+ , testCase "citext" . testCIText
74+ , testCase "JSON" . testJSON
75+ , testCase "Aeson newtype" . testAeson
76+ , testCase "DerivingVia" . testDerivingVia
77+ , testCase "Question mark escape" . testQM
78+ , testCase "Savepoint" . testSavepoint
79+ , testCase "Unicode" . testUnicode
80+ , testCase "Values" . testValues
81+ , testCase "Copy" . testCopy
6182 , testCopyFailures
62− , testCase "Double" . testDouble
63− , testCase "1-ary generic" . testGeneric1
64− , testCase "2-ary generic" . testGeneric2
65− , testCase "3-ary generic" . testGeneric3
66− , testCase "Timeout" . testTimeout
83+ , testCase "Double" . testDouble
84+ , testCase "1-ary generic" . testGeneric1
85+ , testCase "2-ary generic" . testGeneric2
86+ , testCase "3-ary generic" . testGeneric3
87+ , testCase "Timeout" . testTimeout
88+ , testCase "Exceptions" . testExceptions
6789 ]
6890
6991testBytea :: TestEnv -> TestTree
@@ -117,7 +139,7 @@ testFold TestEnv{..} = do
117139 xs <- readIORef ref
118140 writeIORef ref $! (a,b):xs
119141 xs <- readIORef ref
120− reverse xs @?= [(a,b) | a <- [1..100], b <- [1..50]]
142+ reverse xs @?= [(a,b) | b <- [1..50], a <- [1..100]]
121143
122144 -- Make sure fold propagates our exception.
123145 ref <- newIORef []
@@ -234,6 +256,90 @@ testJSON TestEnv{..} = do
234256 js' <- query conn "SELECT ?::json" js
235257 [js] @?= js'
236258
259+testAeson :: TestEnv -> Assertion
260+testAeson TestEnv{..} = do
261+ roundTrip (Map.fromList [] :: Map Text Text)
262+ roundTrip (Map.fromList [("foo","bar"),("bar","baz"),("baz","hello")] :: Map Text Text)
263+ roundTrip (Map.fromList [("fo\"o","bar"),("b\\ar","baz"),("baz","\"value\\with\"escapes")] :: Map Text Text)
264+ roundTrip (V.fromList [1,2,3,4,5::Int])
265+ roundTrip ("foo" :: Text)
266+ roundTrip (42 :: Int)
267+ where
268+ roundTrip :: (Eq a, Show a, Typeable a, ToJSON a, FromJSON a)=> a -> Assertion
269+ roundTrip x = do
270+ y <- query conn "SELECT ?::json" (Only (Aeson x))
271+ [Only (Aeson x)] @?= y
272+
273+testDerivingVia :: TestEnv -> Assertion
274+testDerivingVia TestEnv{..} = do
275+#if __GLASGOW_HASKELL__ <806
276+ return ()
277+#else
278+ roundTrip $ DerivingVia1 42 "Meaning of Life"
279+ where
280+ roundTrip :: (Eq a, Show a, Typeable a, ToField a, FromField a)=> a -> Assertion
281+ roundTrip x = do
282+ y <- query conn "SELECT ?::json" (Only x)
283+ [Only x] @?= y
284+
285+data DerivingVia1 = DerivingVia1 Int String
286+ deriving stock (Eq, Show, Generic)
287+ deriving anyclass (FromJSON, ToJSON)
288+ deriving (ToField, FromField) via Aeson DerivingVia1
289+
290+#endif
291+
292+
293+testQM :: TestEnv -> Assertion
294+testQM TestEnv{..} = do
295+ -- Just test on a single string
296+ let testQuery' b = "testing for ?" <> b <> " and making sure "
297+ testQueryDoubleQM = testQuery' "?"
298+ testQueryRest = "? is substituted"
299+ testQuery = fromString $ testQueryDoubleQM <> testQueryRest
300+ -- expect the entire first part with double QMs replaced with literal '?'
301+ expected = (fromString $ testQuery' "", fromString testQueryRest)
302+ tried = breakOnSingleQuestionMark testQuery
303+ errMsg = concat
304+ [ "Failed to break on single question mark exclusively:\n"
305+ , "expected: ", show expected
306+ , "result: ", show tried
307+ ]
308+ assertBool errMsg $ tried == expected
309+
310+ -- Let's also test the question mark operators in action
311+ -- ? -> Does the string exist as a top-level key within the JSON value?
312+ positiveQuery "SELECT ?::jsonb ?? ?" (testObj, "foo" :: Text)
313+ negativeQuery "SELECT ?::jsonb ?? ?" (testObj, "baz" :: Text)
314+ negativeQuery "SELECT ?::jsonb ?? ?" (toJSON numArray, "1" :: Text)
315+ -- ?| -> Do any of these array strings exist as top-level keys?
316+ positiveQuery "SELECT ?::jsonb ??| ?" (testObj, PGArray ["nope","bar","6" :: Text])
317+ negativeQuery "SELECT ?::jsonb ??| ?" (testObj, PGArray ["nope","6" :: Text])
318+ negativeQuery "SELECT ?::jsonb ??| ?" (toJSON numArray, PGArray ["1","2","6" :: Text])
319+ -- ?& -> Do all of these array strings exist as top-level keys?
320+ positiveQuery "SELECT ?::jsonb ??& ?" (testObj, PGArray ["foo","bar","quux" :: Text])
321+ positiveQuery "SELECT ?::jsonb ??& ?" (testObj, PGArray ["foo","bar" :: Text])
322+ negativeQuery "SELECT ?::jsonb ??& ?" (testObj, PGArray ["foo","bar","baz" :: Text])
323+ negativeQuery "SELECT ?::jsonb ??& ?" (toJSON numArray, PGArray ["1","2","3","4","5" :: Text])
324+ -- Format error for 2 question marks, not 4
325+ True <- expectError (isFormatError 2) $
326+ (query conn "SELECT ?::jsonb ?? ?" $ Only testObj :: IO [Only Bool])
327+ return ()
328+ where positiveQuery :: ToRow a => Query -> a -> Assertion
329+ positiveQuery = boolQuery True
330+ negativeQuery :: ToRow a => Query -> a -> Assertion
331+ negativeQuery = boolQuery False
332+ numArray :: [Int]
333+ numArray = [1,2,3,4,5]
334+ boolQuery :: ToRow a => Bool -> Query -> a -> Assertion
335+ boolQuery b t x = do
336+ a <- query conn t x
337+ [Only b] @?= a
338+ testObj = toJSON (Map.fromList [("foo",toJSON (1 :: Int))
339+ ,("bar",String "baz")
340+ ,("quux",toJSON [1 :: Int,2,3,4,5])] :: Map Text Value
341+ )
342+
237343testSavepoint :: TestEnv -> Assertion
238344testSavepoint TestEnv{..} = do
239345 True <- expectError ST.isNoActiveTransactionError $
@@ -341,6 +447,14 @@ testCopy TestEnv{..} = do
341447 -- so that we can issue more queries:
342448 [Only (x::Int)] <- query_ conn "SELECT 2 + 2"
343449 x @?= 4
450+ -- foldCopyData
451+ copy_ conn "COPY copy_test TO STDOUT (FORMAT CSV)"
452+ (acc, count) <- foldCopyData conn
453+ (\acc row -> return (row:acc))
454+ (\acc count -> return (acc, count))
455+ []
456+ sort acc @?= sort copyRows
457+ count @?= 2
344458 where
345459 copyRows = ["1,foo\n"
346460 ,"2,bar\n"]
@@ -360,12 +474,13 @@ testCopyFailures env = testGroup "Copy failures"
360474
361475goldenTest :: TestName -> IO BL.ByteString -> TestTree
362476goldenTest testName =
363− goldenVsString testName (resultsDir </> fileName<.>"expected")
477+ goldenVsStringDiff testName diff (resultsDir </> fileName<.>"expected")
364478 where
365479 resultsDir = "test" </> "results"
366480 fileName = map normalize testName
367481 normalize c | not (isAlpha c) = '-'
368482 | otherwise = c
483+ diff ref new = ["diff", "-u", ref, new]
369484
370485-- | Test that we provide a sensible error message on failure
371486testCopyUniqueConstraintError :: TestEnv -> TestTree
@@ -478,24 +593,56 @@ isUniqueViolation SqlError{..} = sqlState == "23505"
478593isSyntaxError :: SqlError -> Bool
479594isSyntaxError SqlError{..} = sqlState == "42601"
480595
596+isFormatError :: Int -> FormatError -> Bool
597+isFormatError i FormatError{..}
598+ | null fmtMessage = False
599+ | otherwise = fmtMessage == concat [ show i
600+ , " single '?' characters, but "
601+ , show (length fmtParams)
602+ , " parameters"
603+ ]
481604------------------------------------------------------------------------
482605
483606-- | Action for connecting to the database that will be used for testing.
484607--
485608-- Note that some tests, such as Notify, use multiple connections, and assume
486609-- that 'testConnect' connects to the same database every time it is called.
487−testConnect :: IO Connection
488−testConnect = connectPostgreSQL ""
610+withTestEnv :: ByteString -> (TestEnv -> IO a) -> IO a
611+withTestEnv connstr cb =
612+ withConn $ \conn -> do
613+ -- currently required for interval to work.
614+ -- we also test that this doesn't interfere with anything else
615+ execute_ conn "SET intervalstyle TO 'iso_8601'"
489616
490−withTestEnv :: (TestEnv -> IO a) -> IO a
491−withTestEnv cb =
492− withConn $ \conn ->
493617 cb TestEnv
494618 { conn = conn
495619 , withConn = withConn
496620 }
497621 where
498− withConn = bracket testConnect close
622+ withConn = bracket (connectPostgreSQL connstr) close
499623
500624main :: IO ()
501−main = withTestEnv $ defaultMain . tests
625+main = withConnstring $ \connstring -> do
626+ withTestEnv connstring (defaultMain . tests)
627+
628+withConnstring :: (BS8.ByteString -> IO ()) -> IO ()
629+withConnstring kont = do
630+ env <- getEnvironment
631+ case lookup "DATABASE_CONNSTRING" env of
632+ Just s -> kont (BS8.pack (special s))
633+ Nothing -> case lookup "GITHUB_ACTIONS" env of
634+ Just "true" -> kont (BS8.pack gha)
635+ _ -> putStrLn "Set DATABASE_CONNSTRING environment variable"
636+ where
637+ -- https://www.appveyor.com/docs/services-databases/
638+ special "appveyor" = "dbname='TestDb' user='postgres' password='Password12!'"
639+ special "travis" = ""
640+ special s = s
641+
642+ gha = unwords
643+ [ "dbname='postgres'"
644+ , "user='postgres'"
645+ , "password='postgres'"
646+ , "host='postgres'"
647+ , "port=5432"
648+ ]
test/Time.hs+1−1
@@ -36,7 +36,7 @@ module Time (testTime) where
3636
3737import Common
3838import Control.Monad(forM_, replicateM_)
39−import Data.Time
39+import Data.Time.Compat
4040import Data.ByteString(ByteString)
4141import Database.PostgreSQL.Simple.SqlQQ
4242
test/results/malformed-input.expected+1−1
@@ -1,4 +1,4 @@
11user error (Database.PostgreSQL.Simple.Copy.putCopyEnd: failed to parse command status
2−Connection error: ERROR: invalid input syntax for integer: "z"
2+Connection error: ERROR: invalid input syntax for type integer: "z"
33CONTEXT: COPY copy_unique_constraint_error_test, line 3, column x: "z"
44)