├── .github
└── workflows
│ ├── ci.yml
│ └── hexpm-release.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── EPLICENSE
├── Makefile
├── README.md
├── rebar.config
├── rebar.config.script
└── src
├── p1_pgsql.app.src
├── pgsql.erl
├── pgsql_app.erl
├── pgsql_proto.erl
├── pgsql_sasl.erl
├── pgsql_socket.erl
├── pgsql_sup.erl
└── pgsql_util.erl
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 |
7 | tests:
8 | name: Tests
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | otp: ['20', '25', '26', '27', '28']
13 | runs-on: ubuntu-24.04
14 | container:
15 | image: public.ecr.aws/docker/library/erlang:${{ matrix.otp }}
16 | steps:
17 | - uses: actions/checkout@v4
18 | - run: make
19 | - run: rebar3 compile
20 | - run: rebar3 xref
21 | - run: rebar3 dialyzer
22 | - run: rebar3 eunit -v
23 | - name: Send to Coveralls
24 | if: matrix.otp == 27
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 | run: |
28 | COVERALLS=true rebar3 as test coveralls send
29 | curl -v -k https://coveralls.io/webhook \
30 | --header "Content-Type: application/json" \
31 | --data '{"repo_name":"$GITHUB_REPOSITORY",
32 | "repo_token":"$GITHUB_TOKEN",
33 | "payload":{"build_num":$GITHUB_RUN_ID,
34 | "status":"done"}}'
35 |
--------------------------------------------------------------------------------
/.github/workflows/hexpm-release.yml:
--------------------------------------------------------------------------------
1 | name: Hex
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-24.04
11 | steps:
12 |
13 | - name: Check out
14 | uses: actions/checkout@v4
15 |
16 | - name: Get Erlang/OTP
17 | uses: erlef/setup-beam@v1
18 | with:
19 | otp-version: 26
20 | rebar3-version: '3.24.0'
21 |
22 | - name: Setup rebar3 hex
23 | run: |
24 | mkdir -p ~/.config/rebar3/
25 | echo "{plugins, [rebar3_hex]}." > ~/.config/rebar3/rebar.config
26 |
27 | - run: rebar3 edoc
28 |
29 | - name: Prepare Markdown
30 | run: |
31 | echo "" >>README.md
32 | echo "## EDoc documentation" >>README.md
33 | echo "" >>README.md
34 | echo "You can check this library's " >>README.md
35 | echo "[EDoc documentation](edoc.html), " >>README.md
36 | echo "generated automatically from the source code comments." >>README.md
37 |
38 | - name: Convert Markdown to HTML
39 | uses: natescherer/markdown-to-html-with-github-style-action@v1.1.0
40 | with:
41 | path: README.md
42 |
43 | - run: |
44 | mv doc/index.html doc/edoc.html
45 | mv README.html doc/index.html
46 |
47 | - name: Publish to hex.pm
48 | run: DEBUG=1 rebar3 hex publish --repo hexpm --yes
49 | env:
50 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swo
2 | *.swp
3 | .eunit
4 | .rebar
5 | _build
6 | autom4te.cache
7 | c_src/*.d
8 | c_src/*.gcda
9 | c_src/*.gcno
10 | c_src/*.o
11 | config.log
12 | config.status
13 | deps
14 | ebin
15 | priv
16 | rebar.lock
17 | test/*.beam
18 | vars.config
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Version 1.1.33
2 |
3 | * Updating xmpp to version 1.10.1.
4 |
5 | # Version 1.1.32
6 |
7 | * Updating xmpp to version 1.10.0.
8 |
9 | # Version 1.1.31
10 |
11 | * Updating xmpp to version 1.9.2.
12 |
13 | # Version 1.1.30
14 |
15 | * Updating xmpp to version a1dd8d3ab94fd251f20598e6f002eba38905e218.
16 |
17 | # Version 1.1.29
18 |
19 | * Updating xmpp to version 1.9.1.
20 |
21 | # Version 1.1.28
22 |
23 | * Updating xmpp to version 1.9.0.
24 |
25 | # Version 1.1.27
26 |
27 | * Updating xmpp to version 9728433608e25d196a49ed0f609208e353621135.
28 |
29 | # Version 1.1.26
30 |
31 | * Updating xmpp to version 1.8.2.
32 | * Fix issues with building on OTP27
33 | * Add support for unix domain sockets
34 |
35 | # Version 1.1.25
36 |
37 | * Updating xmpp to version 1.8.1.
38 |
39 | # Version 1.1.24
40 |
41 | * Updating xmpp to version 1.8.0.
42 |
43 | # Version 1.1.23
44 |
45 | * Updating xmpp to version 1.7.0.
46 |
47 | # Version 1.1.22
48 |
49 | * Fix version of xmpp
50 |
51 | # Version 1.1.21
52 |
53 | * Updating xmpp to version 46b6c192d353e60e13a5cf8ed467c5696729b624.
54 |
55 | # Version 1.1.20
56 |
57 | * Updating xmpp to version 1.6.1.
58 |
59 | # Version 1.1.19
60 |
61 | * Updating xmpp to version 1.6.0.
62 |
63 | # Version 1.1.18
64 |
65 | * Updating xmpp to version 1.5.8.
66 |
67 | # Version 1.1.17
68 |
69 | * Updating xmpp to version 1.5.7.
70 |
71 | # Version 1.1.16
72 |
73 | * Updating xmpp to version 1.5.6.
74 |
75 | # Version 1.1.15
76 |
77 | * Fix 'make' compilation with rebar2
78 |
79 | # Version 1.1.14
80 |
81 | * Fix versioning and hex.pm docs publish
82 |
83 | # Version 1.1.13
84 |
85 | * Add SASL auth support
86 |
87 | # Version 1.1.12
88 |
89 | * Switch from using Travis to Github Actions as CI
90 |
91 | # Version 1.1.11
92 |
93 | * Update copyright year to 2021
94 | * recv_byte returns {ok, _} or throws an error, but never returns {error, _}
95 |
96 | # Version 1.1.10
97 |
98 | * Fix Coveralls command call
99 | * Fix Travis setup using Rebar3
100 |
101 | # Version 1.1.9
102 |
103 | * Update copyright to 2020
104 |
105 | # Version 1.1.8
106 |
107 | * Update for hex.pm release
108 |
109 | # Version 1.1.7
110 |
111 | * Add contribution guide
112 |
113 | # Version 1.1.6
114 |
115 | * Add support for ipv6 connections
116 |
117 | # Version 1.1.5
118 |
119 | * Fix compilation with rebar3
120 |
121 | # Version 1.1.4
122 |
123 | * Update coverall script
124 |
125 | # Version 1.1.3
126 |
127 | * Make it possible to set connect timeout
128 |
129 | # Version 1.1.2
130 |
131 | * Add SSL support (Evgeniy Khramtsov)
132 |
133 | # Version 1.1.1
134 |
135 | * adding timeout to pgsql:squery (Felipe Ripoll)
136 | * Decode int4 and int8 as signed integers (Alexey Shchepin)
137 | * Better error handling in pgsql:squery (Alexey Shchepin)
138 |
139 | # Version 1.1.0
140 |
141 | * Fixes 'prepare' and 'execute' calls (Alexey Shchepin)
142 | * Refactor (Alexey Shchepin)
143 |
144 | # Version 1.0.1
145 |
146 | * Repository is now called p1_pgsql for consistency (Mickaël Rémond)
147 | * Initial release on Hex.pm (Mickaël Rémond)
148 | * Standard ProcessOne build chain (Mickaël Rémond)
149 | * Setup Travis-CI and test coverage, tests still needed (Mickaël Rémond)
150 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at contact@process-one.net. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We'd love for you to contribute to our source code and to make our project even better than it is
4 | today! Here are the guidelines we'd like you to follow:
5 |
6 | * [Code of Conduct](#coc)
7 | * [Questions and Problems](#question)
8 | * [Issues and Bugs](#issue)
9 | * [Feature Requests](#feature)
10 | * [Issue Submission Guidelines](#submit)
11 | * [Pull Request Submission Guidelines](#submit-pr)
12 | * [Signing the CLA](#cla)
13 |
14 | ## Code of Conduct
15 |
16 | Help us keep our community open-minded and inclusive. Please read and follow our [Code of Conduct][coc].
17 |
18 | ## Questions, Bugs, Features
19 |
20 | ### Got a Question or Problem?
21 |
22 | Do not open issues for general support questions as we want to keep GitHub issues for bug reports
23 | and feature requests. You've got much better chances of getting your question answered on dedicated
24 | support platforms, the best being [Stack Overflow][stackoverflow].
25 |
26 | Stack Overflow is a much better place to ask questions since:
27 |
28 | - there are thousands of people willing to help on Stack Overflow
29 | - questions and answers stay available for public viewing so your question / answer might help
30 | someone else
31 | - Stack Overflow's voting system assures that the best answers are prominently visible.
32 |
33 | To save your and our time, we will systematically close all issues that are requests for general
34 | support and redirect people to the section you are reading right now.
35 |
36 | ### Found an Issue or Bug?
37 |
38 | If you find a bug in the source code, you can help us by submitting an issue to our
39 | [GitHub Repository][github]. Even better, you can submit a Pull Request with a fix.
40 |
41 | ### Missing a Feature?
42 |
43 | You can request a new feature by submitting an issue to our [GitHub Repository][github-issues].
44 |
45 | If you would like to implement a new feature then consider what kind of change it is:
46 |
47 | * **Major Changes** that you wish to contribute to the project should be discussed first in an
48 | [GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature.
49 | * **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github]
50 | as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr).
51 |
52 | ## Issue Submission Guidelines
53 |
54 | Before you submit your issue search the archive, maybe your question was already answered.
55 |
56 | If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize
57 | the effort we can spend fixing issues and adding new features, by not reporting duplicate issues.
58 |
59 | The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to
60 | make it easier to understand and categorize the issue.
61 |
62 | ## Pull Request Submission Guidelines
63 |
64 | By submitting a pull request for a code or doc contribution, you need to have the right
65 | to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla]
66 | for details.
67 |
68 | Before you submit your pull request consider the following guidelines:
69 |
70 | * Search [GitHub][github-pr] for an open or closed Pull Request
71 | that relates to your submission. You don't want to duplicate effort.
72 | * Make your changes in a new git branch:
73 |
74 | ```shell
75 | git checkout -b my-fix-branch master
76 | ```
77 | * Test your changes and, if relevant, expand the automated test suite.
78 | * Create your patch commit, including appropriate test cases.
79 | * If the changes affect public APIs, change or add relevant documentation.
80 | * Commit your changes using a descriptive commit message.
81 |
82 | ```shell
83 | git commit -a
84 | ```
85 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
86 |
87 | * Push your branch to GitHub:
88 |
89 | ```shell
90 | git push origin my-fix-branch
91 | ```
92 |
93 | * In GitHub, send a pull request to `master` branch. This will trigger the continuous integration and run the test.
94 | We will also notify you if you have not yet signed the [contribution agreement][cla].
95 |
96 | * If you find that the continunous integration has failed, look into the logs to find out
97 | if your changes caused test failures, the commit message was malformed etc. If you find that the
98 | tests failed or times out for unrelated reasons, you can ping a team member so that the build can be
99 | restarted.
100 |
101 | * If we suggest changes, then:
102 |
103 | * Make the required updates.
104 | * Test your changes and test cases.
105 | * Commit your changes to your branch (e.g. `my-fix-branch`).
106 | * Push the changes to your GitHub repository (this will update your Pull Request).
107 |
108 | You can also amend the initial commits and force push them to the branch.
109 |
110 | ```shell
111 | git rebase master -i
112 | git push origin my-fix-branch -f
113 | ```
114 |
115 | This is generally easier to follow, but separate commits are useful if the Pull Request contains
116 | iterations that might be interesting to see side-by-side.
117 |
118 | That's it! Thank you for your contribution!
119 |
120 | ## Signing the Contributor License Agreement (CLA)
121 |
122 | Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done
123 | so before. It's a quick process, we promise, and you will be able to do it all online
124 |
125 | You can read [ProcessOne Contribution License Agreement][cla] in PDF.
126 |
127 | This is part of the legal framework of the open-source ecosystem that adds some red tape,
128 | but protects both the contributor and the company / foundation behind the project. It also
129 | gives us the option to relicense the code with a more permissive license in the future.
130 |
131 |
132 | [coc]: https://github.com/processone/p1_pgsql/blob/master/CODE_OF_CONDUCT.md
133 | [stackoverflow]: https://stackoverflow.com/
134 | [github]: https://github.com/processone/p1_pgsql
135 | [github-issues]: https://github.com/processone/p1_pgsql/issues
136 | [github-new-issue]: https://github.com/processone/p1_pgsql/issues/new
137 | [github-pr]: https://github.com/processone/p1_pgsql/pulls
138 | [cla]: https://www.process-one.net/resources/ejabberd-cla.pdf
139 | [license]: https://github.com/processone/p1_pgsql/blob/master/EPLICENSE
140 |
--------------------------------------------------------------------------------
/EPLICENSE:
--------------------------------------------------------------------------------
1 | ERLANG PUBLIC LICENSE
2 | Version 1.1
3 |
4 | 1. Definitions.
5 |
6 | 1.1. ``Contributor'' means each entity that creates or contributes to
7 | the creation of Modifications.
8 |
9 | 1.2. ``Contributor Version'' means the combination of the Original
10 | Code, prior Modifications used by a Contributor, and the Modifications
11 | made by that particular Contributor.
12 |
13 | 1.3. ``Covered Code'' means the Original Code or Modifications or the
14 | combination of the Original Code and Modifications, in each case
15 | including portions thereof.
16 |
17 | 1.4. ``Electronic Distribution Mechanism'' means a mechanism generally
18 | accepted in the software development community for the electronic
19 | transfer of data.
20 |
21 | 1.5. ``Executable'' means Covered Code in any form other than Source
22 | Code.
23 |
24 | 1.6. ``Initial Developer'' means the individual or entity identified
25 | as the Initial Developer in the Source Code notice required by Exhibit
26 | A.
27 |
28 | 1.7. ``Larger Work'' means a work which combines Covered Code or
29 | portions thereof with code not governed by the terms of this License.
30 |
31 | 1.8. ``License'' means this document.
32 |
33 | 1.9. ``Modifications'' means any addition to or deletion from the
34 | substance or structure of either the Original Code or any previous
35 | Modifications. When Covered Code is released as a series of files, a
36 | Modification is:
37 |
38 | A. Any addition to or deletion from the contents of a file containing
39 | Original Code or previous Modifications.
40 |
41 | B. Any new file that contains any part of the Original Code or
42 | previous Modifications.
43 |
44 | 1.10. ``Original Code'' means Source Code of computer software code
45 | which is described in the Source Code notice required by Exhibit A as
46 | Original Code, and which, at the time of its release under this
47 | License is not already Covered Code governed by this License.
48 |
49 | 1.11. ``Source Code'' means the preferred form of the Covered Code for
50 | making modifications to it, including all modules it contains, plus
51 | any associated interface definition files, scripts used to control
52 | compilation and installation of an Executable, or a list of source
53 | code differential comparisons against either the Original Code or
54 | another well known, available Covered Code of the Contributor's
55 | choice. The Source Code can be in a compressed or archival form,
56 | provided the appropriate decompression or de-archiving software is
57 | widely available for no charge.
58 |
59 | 1.12. ``You'' means an individual or a legal entity exercising rights
60 | under, and complying with all of the terms of, this License. For legal
61 | entities,``You'' includes any entity which controls, is controlled by,
62 | or is under common control with You. For purposes of this definition,
63 | ``control'' means (a) the power, direct or indirect, to cause the
64 | direction or management of such entity, whether by contract or
65 | otherwise, or (b) ownership of fifty percent (50%) or more of the
66 | outstanding shares or beneficial ownership of such entity.
67 |
68 | 2. Source Code License.
69 |
70 | 2.1. The Initial Developer Grant.
71 | The Initial Developer hereby grants You a world-wide, royalty-free,
72 | non-exclusive license, subject to third party intellectual property
73 | claims:
74 |
75 | (a) to use, reproduce, modify, display, perform, sublicense and
76 | distribute the Original Code (or portions thereof) with or without
77 | Modifications, or as part of a Larger Work; and
78 |
79 | (b) under patents now or hereafter owned or controlled by Initial
80 | Developer, to make, have made, use and sell (``Utilize'') the
81 | Original Code (or portions thereof), but solely to the extent that
82 | any such patent is reasonably necessary to enable You to Utilize
83 | the Original Code (or portions thereof) and not to any greater
84 | extent that may be necessary to Utilize further Modifications or
85 | combinations.
86 |
87 | 2.2. Contributor Grant.
88 | Each Contributor hereby grants You a world-wide, royalty-free,
89 | non-exclusive license, subject to third party intellectual property
90 | claims:
91 |
92 | (a) to use, reproduce, modify, display, perform, sublicense and
93 | distribute the Modifications created by such Contributor (or
94 | portions thereof) either on an unmodified basis, with other
95 | Modifications, as Covered Code or as part of a Larger Work; and
96 |
97 | (b) under patents now or hereafter owned or controlled by Contributor,
98 | to Utilize the Contributor Version (or portions thereof), but
99 | solely to the extent that any such patent is reasonably necessary
100 | to enable You to Utilize the Contributor Version (or portions
101 | thereof), and not to any greater extent that may be necessary to
102 | Utilize further Modifications or combinations.
103 |
104 | 3. Distribution Obligations.
105 |
106 | 3.1. Application of License.
107 | The Modifications which You contribute are governed by the terms of
108 | this License, including without limitation Section 2.2. The Source
109 | Code version of Covered Code may be distributed only under the terms
110 | of this License, and You must include a copy of this License with
111 | every copy of the Source Code You distribute. You may not offer or
112 | impose any terms on any Source Code version that alters or restricts
113 | the applicable version of this License or the recipients' rights
114 | hereunder. However, You may include an additional document offering
115 | the additional rights described in Section 3.5.
116 |
117 | 3.2. Availability of Source Code.
118 | Any Modification which You contribute must be made available in Source
119 | Code form under the terms of this License either on the same media as
120 | an Executable version or via an accepted Electronic Distribution
121 | Mechanism to anyone to whom you made an Executable version available;
122 | and if made available via Electronic Distribution Mechanism, must
123 | remain available for at least twelve (12) months after the date it
124 | initially became available, or at least six (6) months after a
125 | subsequent version of that particular Modification has been made
126 | available to such recipients. You are responsible for ensuring that
127 | the Source Code version remains available even if the Electronic
128 | Distribution Mechanism is maintained by a third party.
129 |
130 | 3.3. Description of Modifications.
131 | You must cause all Covered Code to which you contribute to contain a
132 | file documenting the changes You made to create that Covered Code and
133 | the date of any change. You must include a prominent statement that
134 | the Modification is derived, directly or indirectly, from Original
135 | Code provided by the Initial Developer and including the name of the
136 | Initial Developer in (a) the Source Code, and (b) in any notice in an
137 | Executable version or related documentation in which You describe the
138 | origin or ownership of the Covered Code.
139 |
140 | 3.4. Intellectual Property Matters
141 |
142 | (a) Third Party Claims.
143 | If You have knowledge that a party claims an intellectual property
144 | right in particular functionality or code (or its utilization
145 | under this License), you must include a text file with the source
146 | code distribution titled ``LEGAL'' which describes the claim and
147 | the party making the claim in sufficient detail that a recipient
148 | will know whom to contact. If you obtain such knowledge after You
149 | make Your Modification available as described in Section 3.2, You
150 | shall promptly modify the LEGAL file in all copies You make
151 | available thereafter and shall take other steps (such as notifying
152 | appropriate mailing lists or newsgroups) reasonably calculated to
153 | inform those who received the Covered Code that new knowledge has
154 | been obtained.
155 |
156 | (b) Contributor APIs.
157 | If Your Modification is an application programming interface and
158 | You own or control patents which are reasonably necessary to
159 | implement that API, you must also include this information in the
160 | LEGAL file.
161 |
162 | 3.5. Required Notices.
163 | You must duplicate the notice in Exhibit A in each file of the Source
164 | Code, and this License in any documentation for the Source Code, where
165 | You describe recipients' rights relating to Covered Code. If You
166 | created one or more Modification(s), You may add your name as a
167 | Contributor to the notice described in Exhibit A. If it is not
168 | possible to put such notice in a particular Source Code file due to
169 | its structure, then you must include such notice in a location (such
170 | as a relevant directory file) where a user would be likely to look for
171 | such a notice. You may choose to offer, and to charge a fee for,
172 | warranty, support, indemnity or liability obligations to one or more
173 | recipients of Covered Code. However, You may do so only on Your own
174 | behalf, and not on behalf of the Initial Developer or any
175 | Contributor. You must make it absolutely clear than any such warranty,
176 | support, indemnity or liability obligation is offered by You alone,
177 | and You hereby agree to indemnify the Initial Developer and every
178 | Contributor for any liability incurred by the Initial Developer or
179 | such Contributor as a result of warranty, support, indemnity or
180 | liability terms You offer.
181 |
182 | 3.6. Distribution of Executable Versions.
183 | You may distribute Covered Code in Executable form only if the
184 | requirements of Section 3.1-3.5 have been met for that Covered Code,
185 | and if You include a notice stating that the Source Code version of
186 | the Covered Code is available under the terms of this License,
187 | including a description of how and where You have fulfilled the
188 | obligations of Section 3.2. The notice must be conspicuously included
189 | in any notice in an Executable version, related documentation or
190 | collateral in which You describe recipients' rights relating to the
191 | Covered Code. You may distribute the Executable version of Covered
192 | Code under a license of Your choice, which may contain terms different
193 | from this License, provided that You are in compliance with the terms
194 | of this License and that the license for the Executable version does
195 | not attempt to limit or alter the recipient's rights in the Source
196 | Code version from the rights set forth in this License. If You
197 | distribute the Executable version under a different license You must
198 | make it absolutely clear that any terms which differ from this License
199 | are offered by You alone, not by the Initial Developer or any
200 | Contributor. You hereby agree to indemnify the Initial Developer and
201 | every Contributor for any liability incurred by the Initial Developer
202 | or such Contributor as a result of any such terms You offer.
203 |
204 | 3.7. Larger Works.
205 | You may create a Larger Work by combining Covered Code with other code
206 | not governed by the terms of this License and distribute the Larger
207 | Work as a single product. In such a case, You must make sure the
208 | requirements of this License are fulfilled for the Covered Code.
209 |
210 | 4. Inability to Comply Due to Statute or Regulation.
211 | If it is impossible for You to comply with any of the terms of this
212 | License with respect to some or all of the Covered Code due to statute
213 | or regulation then You must: (a) comply with the terms of this License
214 | to the maximum extent possible; and (b) describe the limitations and
215 | the code they affect. Such description must be included in the LEGAL
216 | file described in Section 3.4 and must be included with all
217 | distributions of the Source Code. Except to the extent prohibited by
218 | statute or regulation, such description must be sufficiently detailed
219 | for a recipient of ordinary skill to be able to understand it.
220 |
221 | 5. Application of this License.
222 |
223 | This License applies to code to which the Initial Developer has
224 | attached the notice in Exhibit A, and to related Covered Code.
225 |
226 | 6. CONNECTION TO MOZILLA PUBLIC LICENSE
227 |
228 | This Erlang License is a derivative work of the Mozilla Public
229 | License, Version 1.0. It contains terms which differ from the Mozilla
230 | Public License, Version 1.0.
231 |
232 | 7. DISCLAIMER OF WARRANTY.
233 |
234 | COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN ``AS IS'' BASIS,
235 | WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
236 | WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
237 | DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR
238 | NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF
239 | THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE
240 | IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER
241 | CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR
242 | CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART
243 | OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER
244 | EXCEPT UNDER THIS DISCLAIMER.
245 |
246 | 8. TERMINATION.
247 | This License and the rights granted hereunder will terminate
248 | automatically if You fail to comply with terms herein and fail to cure
249 | such breach within 30 days of becoming aware of the breach. All
250 | sublicenses to the Covered Code which are properly granted shall
251 | survive any termination of this License. Provisions which, by their
252 | nature, must remain in effect beyond the termination of this License
253 | shall survive.
254 |
255 | 9. DISCLAIMER OF LIABILITY
256 | Any utilization of Covered Code shall not cause the Initial Developer
257 | or any Contributor to be liable for any damages (neither direct nor
258 | indirect).
259 |
260 | 10. MISCELLANEOUS
261 | This License represents the complete agreement concerning the subject
262 | matter hereof. If any provision is held to be unenforceable, such
263 | provision shall be reformed only to the extent necessary to make it
264 | enforceable. This License shall be construed by and in accordance with
265 | the substantive laws of Sweden. Any dispute, controversy or claim
266 | arising out of or relating to this License, or the breach, termination
267 | or invalidity thereof, shall be subject to the exclusive jurisdiction
268 | of Swedish courts, with the Stockholm City Court as the first
269 | instance.
270 |
271 | EXHIBIT A.
272 |
273 | ``The contents of this file are subject to the Erlang Public License,
274 | Version 1.1, (the "License"); you may not use this file except in
275 | compliance with the License. You should have received a copy of the
276 | Erlang Public License along with this software. If not, it can be
277 | retrieved via the world wide web at http://www.erlang.org/.
278 |
279 | Software distributed under the License is distributed on an "AS IS"
280 | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
281 | the License for the specific language governing rights and limitations
282 | under the License.
283 |
284 | The Initial Developer of the Original Code is Ericsson Utvecklings AB.
285 | Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
286 | AB. All Rights Reserved.''
287 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | REBAR ?= rebar
2 |
3 | all: src
4 |
5 | src:
6 | $(REBAR) get-deps
7 | $(REBAR) compile
8 |
9 | clean:
10 | $(REBAR) clean
11 |
12 | xref: all
13 | $(REBAR) xref
14 |
15 | .PHONY: clean src
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # p1_pgsql
2 |
3 | [](https://github.com/processone/p1_pgsql/actions/workflows/ci.yml)
4 | [](https://coveralls.io/github/processone/p1_pgsql?branch=master)
5 | [](https://hex.pm/packages/p1_pgsql)
6 |
7 | Pure Erlang PostgreSQL driver
8 |
9 | ## Building
10 |
11 | PostgreSQL driver can be build as follow:
12 |
13 | make
14 |
15 | It is a rebar-compatible OTP application. Alternatively, you can build
16 | it with rebar:
17 |
18 | rebar compile
19 |
20 | ## Development
21 |
22 | ### Test
23 |
24 | #### Unit test
25 |
26 | You can run eunit test with the command:
27 |
28 | $ rebar eunit
29 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %%%----------------------------------------------------------------------
2 | %%% File : rebar.config
3 | %%% Author : Mickael Remond
4 | %%% Purpose : Rebar build script. Compliant with rebar and rebar3.
5 | %%% Created : 15 Dec 2015 by Mickael Remond
6 | %%%
7 | %%% Copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved.
8 | %%%
9 | %%% Licensed under the Apache License, Version 2.0 (the "License");
10 | %%% you may not use this file except in compliance with the License.
11 | %%% You may obtain a copy of the License at
12 | %%%
13 | %%% http://www.apache.org/licenses/LICENSE-2.0
14 | %%%
15 | %%% Unless required by applicable law or agreed to in writing, software
16 | %%% distributed under the License is distributed on an "AS IS" BASIS,
17 | %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | %%% See the License for the specific language governing permissions and
19 | %%% limitations under the License.
20 | %%%
21 | %%%----------------------------------------------------------------------
22 |
23 | {erl_opts, [debug_info, {src_dirs, ["src"]}, {i, "include"}]}.
24 |
25 | {deps, [{xmpp, "~> 1.10.0", {git, "https://github.com/processone/xmpp", {tag, "1.10.1"}}}]}.
26 |
27 | {cover_enabled, true}.
28 | {cover_export_enabled, true}.
29 | {coveralls_coverdata , "_build/test/cover/eunit.coverdata"}.
30 | {coveralls_service_name , "github"}.
31 |
32 | {xref_checks, [undefined_function_calls, undefined_functions, deprecated_function_calls, deprecated_functions]}.
33 |
34 | {profiles, [{test, [{erl_opts, [{src_dirs, ["src", "test"]}]}]}]}.
35 |
36 | %% Local Variables:
37 | %% mode: erlang
38 | %% End:
39 | %% vim: set filetype=erlang tabstop=8:
40 |
--------------------------------------------------------------------------------
/rebar.config.script:
--------------------------------------------------------------------------------
1 | %%%----------------------------------------------------------------------
2 | %%% File : rebar.config.script
3 | %%% Author : Mickael Remond
4 | %%% Purpose : Rebar build script. Compliant with rebar and rebar3.
5 | %%% Created : 24 Nov 2015 by Mickael Remond
6 | %%%
7 | %%% Copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved.
8 | %%%
9 | %%% Licensed under the Apache License, Version 2.0 (the "License");
10 | %%% you may not use this file except in compliance with the License.
11 | %%% You may obtain a copy of the License at
12 | %%%
13 | %%% http://www.apache.org/licenses/LICENSE-2.0
14 | %%%
15 | %%% Unless required by applicable law or agreed to in writing, software
16 | %%% distributed under the License is distributed on an "AS IS" BASIS,
17 | %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | %%% See the License for the specific language governing permissions and
19 | %%% limitations under the License.
20 | %%%
21 | %%%----------------------------------------------------------------------
22 |
23 | IsRebar3 = case application:get_key(rebar, vsn) of
24 | {ok, VSN} ->
25 | [VSN1 | _] = string:tokens(VSN, "-"),
26 | [Maj|_] = string:tokens(VSN1, "."),
27 | (list_to_integer(Maj) >= 3);
28 | undefined ->
29 | lists:keymember(mix, 1, application:loaded_applications())
30 | end,
31 |
32 | ModCfg0 = fun(F, Cfg, [Key|Tail], Op, Default) ->
33 | {OldVal,PartCfg} = case lists:keytake(Key, 1, Cfg) of
34 | {value, {_, V1}, V2} -> {V1, V2};
35 | false -> {if Tail == [] -> Default; true -> [] end, Cfg}
36 | end,
37 | case Tail of
38 | [] ->
39 | [{Key, Op(OldVal)} | PartCfg];
40 | _ ->
41 | [{Key, F(F, OldVal, Tail, Op, Default)} | PartCfg]
42 | end
43 | end,
44 | ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op,
45 | Default) end,
46 |
47 | ModCfgS = fun(Cfg, Keys, Val) -> ModCfg0(ModCfg0, Cfg, Keys, fun(_V) ->
48 | Val end, "") end,
49 |
50 |
51 | FilterConfig = fun(F, Cfg, [{Path, true, ModFun, Default} | Tail]) ->
52 | F(F, ModCfg0(ModCfg0, Cfg, Path, ModFun, Default), Tail);
53 | (F, Cfg, [_ | Tail]) ->
54 | F(F, Cfg, Tail);
55 | (F, Cfg, []) ->
56 | Cfg
57 | end,
58 |
59 | AppendStr = fun(Append) ->
60 | fun("") ->
61 | Append;
62 | (Val) ->
63 | Val ++ " " ++ Append
64 | end
65 | end,
66 | AppendList = fun(Append) ->
67 | fun(Val) ->
68 | Val ++ Append
69 | end
70 | end,
71 |
72 | % Convert our rich deps syntax to rebar2 format:
73 | % https://github.com/rebar/rebar/wiki/Dependency-management
74 | Rebar2DepsFilter =
75 | fun(DepsList) ->
76 | lists:map(fun({DepName, _HexVersion, Source}) ->
77 | {DepName, ".*", Source}
78 | end, DepsList)
79 | end,
80 |
81 | % Convert our rich deps syntax to rebar3 version definition format:
82 | % https://rebar3.org/docs/configuration/dependencies/#dependency-version-handling
83 | % https://hexdocs.pm/elixir/Version.html
84 | Rebar3DepsFilter =
85 | fun(DepsList) ->
86 | lists:map(fun({DepName, HexVersion, {git, _, {tag, GitVersion}} = Source}) ->
87 | case HexVersion == ".*" of
88 | true ->
89 | {DepName, GitVersion};
90 | false ->
91 | {DepName, HexVersion}
92 | end;
93 | ({DepName, _HexVersion, Source}) ->
94 | {DepName, ".*", Source}
95 | end, DepsList)
96 | end,
97 |
98 | GlobalDepsFilter = fun(Deps) ->
99 | DepNames = lists:map(fun({DepName, _, _}) -> DepName;
100 | ({DepName, _}) -> DepName
101 | end, Deps),
102 | lists:filtermap(fun(Dep) ->
103 | case code:lib_dir(Dep) of
104 | {error, _} ->
105 | {true,"Unable to locate dep '"++atom_to_list(Dep)++"' in system deps."};
106 | _ ->
107 | false
108 | end
109 | end, DepNames)
110 | end,
111 |
112 | GithubConfig = case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of
113 | {"true", Token} when is_list(Token) ->
114 | CONFIG1 = [{coveralls_repo_token, Token},
115 | {coveralls_service_job_id, os:getenv("GITHUB_RUN_ID")},
116 | {coveralls_commit_sha, os:getenv("GITHUB_SHA")},
117 | {coveralls_service_number, os:getenv("GITHUB_RUN_NUMBER")}],
118 | case os:getenv("GITHUB_EVENT_NAME") =:= "pull_request"
119 | andalso string:tokens(os:getenv("GITHUB_REF"), "/") of
120 | [_, "pull", PRNO, _] ->
121 | [{coveralls_service_pull_request, PRNO} | CONFIG1];
122 | _ ->
123 | CONFIG1
124 | end;
125 | _ ->
126 | []
127 | end,
128 |
129 | Rules = [
130 | {[deps], (not IsRebar3),
131 | Rebar2DepsFilter, []},
132 | {[deps], IsRebar3,
133 | Rebar3DepsFilter, []},
134 | {[plugins], IsRebar3,
135 | AppendList([pc]), []},
136 | {[plugins], os:getenv("COVERALLS") == "true",
137 | AppendList([{coveralls, {git,
138 | "https://github.com/processone/coveralls-erl.git",
139 | {branch, "addjsonfile"}}} ]), []},
140 | {[deps], os:getenv("USE_GLOBAL_DEPS") /= false,
141 | GlobalDepsFilter, []}
142 | ],
143 |
144 |
145 | Config = FilterConfig(FilterConfig, CONFIG, Rules) ++ GithubConfig,
146 |
147 | %io:format("Rules:~n~p~n~nCONFIG:~n~p~n~nConfig:~n~p~n", [Rules, CONFIG, Config]),
148 |
149 | Config.
150 |
151 | %% Local Variables:
152 | %% mode: erlang
153 | %% End:
154 | %% vim: set filetype=erlang tabstop=8:
155 |
--------------------------------------------------------------------------------
/src/p1_pgsql.app.src:
--------------------------------------------------------------------------------
1 | %%%----------------------------------------------------------------------
2 | %%% File : p1_pgsql.app.src
3 | %%% Author : Evgeniy Khramtsov
4 | %%% Purpose : Application package description
5 | %%% Created : 4 Apr 2013 by Evgeniy Khramtsov
6 | %%%
7 | %%%
8 | %%% Copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved.
9 | %%%
10 | %%% Licensed under the Apache License, Version 2.0 (the "License");
11 | %%% you may not use this file except in compliance with the License.
12 | %%% You may obtain a copy of the License at
13 | %%%
14 | %%% http://www.apache.org/licenses/LICENSE-2.0
15 | %%%
16 | %%% Unless required by applicable law or agreed to in writing, software
17 | %%% distributed under the License is distributed on an "AS IS" BASIS,
18 | %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 | %%% See the License for the specific language governing permissions and
20 | %%% limitations under the License.
21 | %%%
22 | %%%----------------------------------------------------------------------
23 |
24 | {application, p1_pgsql,
25 | [{description, "PostgreSQL driver"},
26 | {vsn, "1.1.33"},
27 | {modules, []},
28 | {registered, []},
29 | {applications, [kernel, stdlib, ssl, xmpp]},
30 | {mod, {pgsql_app,[]}},
31 |
32 | %% hex.pm packaging:
33 | {licenses, ["EPL 1.1", "Apache 2.0"]},
34 | {links, [{"Github", "https://github.com/processone/p1_pgsql"}]}]}.
35 |
36 | %% Local Variables:
37 | %% mode: erlang
38 | %% End:
39 | %% vim: set filetype=erlang tabstop=8:
40 |
--------------------------------------------------------------------------------
/src/pgsql.erl:
--------------------------------------------------------------------------------
1 | %%% File : pgsql.erl
2 | %%% Author : Christian Sunesson
3 | %%% Description : PostgresQL interface
4 | %%% Created : 11 May 2005
5 |
6 | %%
7 | %% API for accessing the postgres driver.
8 | %%
9 |
10 | -module(pgsql).
11 | -export([connect/1, connect/4, connect/5, connect/6]).
12 |
13 | -export([squery/2, squery/3,
14 | pquery/3,
15 | terminate/1,
16 | prepare/3, unprepare/2,
17 | execute/3]).
18 |
19 |
20 | connect(Host, Database, User, Password) ->
21 | connect([{database, Database},
22 | {host, Host},
23 | {user, User},
24 | {password, Password}]).
25 |
26 | connect(Host, Database, User, Password, Port) ->
27 | connect([{database, Database},
28 | {host, Host},
29 | {user, User},
30 | {port, Port},
31 | {password, Password}]).
32 |
33 | connect(Host, Database, User, Password, Port, ConnectTimeout) ->
34 | connect([{database, Database},
35 | {host, Host},
36 | {user, User},
37 | {port, Port},
38 | {password, Password},
39 | {connect_timeout, ConnectTimeout}]).
40 |
41 | connect(Options) ->
42 | pgsql_proto:start(Options).
43 |
44 | %% Close a connection
45 | terminate(Db) ->
46 | gen_server:call(Db, terminate).
47 |
48 | %%% In the "simple query" protocol, the frontend just sends a
49 | %%% textual query string, which is parsed and immediately
50 | %%% executed by the backend.
51 |
52 | %% A simple query can contain multiple statements (separated with a semi-colon),
53 | %% and each statement's response.
54 |
55 | %%% squery(Db, Query) -> {ok, Results} | ... no real error handling
56 | %%% Query = string()
57 | %%% Results = [Result]
58 | %%% Result = {"SELECT", RowDesc, ResultSet} | ...
59 | squery(Db, Query) ->
60 | gen_server:call(Db, {squery, Query}, infinity).
61 |
62 | %%% squery(Db, Query, Timeout) -> {ok, Results} | ... no real error handling
63 | %%% Query = string()
64 | %%% Timeout = milliseconds()
65 | %%% Results = [Result]
66 | %%% Result = {"SELECT", RowDesc, ResultSet} | ...
67 | squery(Db, Query, Timeout) ->
68 | gen_server:call(Db, {squery, Query}, Timeout).
69 |
70 | %%% In the "extended query" protocol, processing of queries is
71 | %%% separated into multiple steps: parsing, binding of parameter
72 | %%% values, and execution. This offers flexibility and performance
73 | %%% benefits, at the cost of extra complexity.
74 |
75 | %%% pquery(Db, Query, Params) -> {ok, Command, Status, NameTypes, Rows} | timeout | ...
76 | %%% Query = string()
77 | %%% Params = [term()]
78 | %%% Command = string()
79 | %%% Status = idle | transaction | failed_transaction
80 | %%% NameTypes = [{ColName, ColType}]
81 | %%% Rows = [list()]
82 | pquery(Db, Query, Params) ->
83 | gen_server:call(Db, {equery, {Query, Params}}).
84 |
85 | %%% prepare(Db, Name, Query) -> {ok, Status, ParamTypes, ResultTypes}
86 | %%% Status = idle | transaction | failed_transaction
87 | %%% ParamTypes = [atom()]
88 | %%% ResultTypes = [{ColName, ColType}]
89 | prepare(Db, Name, Query) ->
90 | gen_server:call(Db, {prepare, {Name, Query}}).
91 |
92 | %%% unprepare(Db, Name) -> ok | timeout | ...
93 | %%% Name = atom()
94 | unprepare(Db, Name) ->
95 | gen_server:call(Db, {unprepare, Name}).
96 |
97 | %%% execute(Db, Name, Params) -> {ok, Result} | timeout | ...
98 | %%% Result = {'INSERT', NRows} |
99 | %%% {'DELETE', NRows} |
100 | %%% {'SELECT', ResultSet} |
101 | %%% ...
102 | %%% ResultSet = [Row]
103 | %%% Row = list()
104 | execute(Db, Name, Params) when is_list(Params) ->
105 | gen_server:call(Db, {execute, {Name, Params}}).
106 |
--------------------------------------------------------------------------------
/src/pgsql_app.erl:
--------------------------------------------------------------------------------
1 | %%%----------------------------------------------------------------------
2 | %%% File : pgsql_app.erl
3 | %%% Author : Evgeniy Khramtsov
4 | %%% Purpose : PostgreSQL erlang driver application
5 | %%% Created : 15 May 2013 by Evgeniy Khramtsov
6 | %%%
7 | %%%
8 | %%% p1_pgsql, Copyright (C) 2002-2025 ProcessOne
9 | %%%
10 | %%% This program is free software; you can redistribute it and/or
11 | %%% modify it under the terms of the GNU General Public License as
12 | %%% published by the Free Software Foundation; either version 2 of the
13 | %%% License, or (at your option) any later version.
14 | %%%
15 | %%% This program is distributed in the hope that it will be useful,
16 | %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 | %%% General Public License for more details.
19 | %%%
20 | %%% You should have received a copy of the GNU General Public License
21 | %%% along with this program; if not, write to the Free Software
22 | %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
23 | %%% 02111-1307 USA
24 | %%%
25 | %%%----------------------------------------------------------------------
26 |
27 | -module(pgsql_app).
28 |
29 | -behaviour(application).
30 |
31 | %% Application callbacks
32 | -export([start/2, stop/1]).
33 |
34 | %%%===================================================================
35 | %%% Application callbacks
36 | %%%===================================================================
37 | start(_StartType, _StartArgs) ->
38 | pgsql_sup:start_link().
39 |
40 | stop(_State) ->
41 | ok.
42 |
43 | %%%===================================================================
44 | %%% Internal functions
45 | %%%===================================================================
46 |
--------------------------------------------------------------------------------
/src/pgsql_proto.erl:
--------------------------------------------------------------------------------
1 | %%% File : pgsql_proto.erl
2 | %%% Author : Christian Sunesson
3 | %%% Description : PostgreSQL protocol driver
4 | %%% Created : 9 May 2005
5 |
6 | %%% This is the protocol handling part of the PostgreSQL driver, it turns packages into
7 | %%% erlang term messages and back.
8 |
9 | -module(pgsql_proto).
10 |
11 | -behaviour(gen_server).
12 |
13 | %% TODO:
14 | %% When factorizing make clear distinction between message and packet.
15 | %% Packet == binary on-wire representation
16 | %% Message = parsed Packet as erlang terms.
17 |
18 | %%% Version 3.0 of the protocol.
19 | %%% Supported in postgres from version 7.4
20 | -define(PROTOCOL_MAJOR, 3).
21 | -define(PROTOCOL_MINOR, 0).
22 | -define(STARTTLS, 80877103).
23 |
24 | %%% PostgreSQL protocol message codes
25 | -define(PG_BACKEND_KEY_DATA, $K).
26 | -define(PG_PARAMETER_STATUS, $S).
27 | -define(PG_ERROR_MESSAGE, $E).
28 | -define(PG_NOTICE_RESPONSE, $N).
29 | -define(PG_EMPTY_RESPONSE, $I).
30 | -define(PG_ROW_DESCRIPTION, $T).
31 | -define(PG_DATA_ROW, $D).
32 | -define(PG_READY_FOR_QUERY, $Z).
33 | -define(PG_AUTHENTICATE, $R).
34 | -define(PG_BIND, $B).
35 | -define(PG_PARSE, $P).
36 | -define(PG_COMMAND_COMPLETE, $C).
37 | -define(PG_PARSE_COMPLETE, $1).
38 | -define(PG_BIND_COMPLETE, $2).
39 | -define(PG_CLOSE_COMPLETE, $3).
40 | -define(PG_PORTAL_SUSPENDED, $s).
41 | -define(PG_NO_DATA, $n).
42 |
43 | -export([start/1, start_link/1]).
44 |
45 | %% gen_server callbacks
46 | -export([init/1,
47 | handle_call/3,
48 | handle_cast/2,
49 | code_change/3,
50 | handle_info/2,
51 | terminate/2]).
52 |
53 | %% For protocol unwrapping, pgsql_socket for example.
54 | -export([decode_packet/3]).
55 | -export([encode_message/2]).
56 | -export([encode/2]).
57 |
58 | -import(pgsql_util, [option/3]).
59 | -import(pgsql_util, [socket/2, close/1, controlling_process/2, starttls/2]).
60 | -import(pgsql_util, [send/2, send_int/2, send_msg/3]).
61 | -import(pgsql_util, [recv_msg/2, recv_msg/1, recv_byte/2, recv_byte/1]).
62 | -import(pgsql_util, [string/1, make_pair/2, split_pair/2]).
63 | -import(pgsql_util, [count_string/1, to_string/2]).
64 | -import(pgsql_util, [coldescs/3, datacoldescs/3]).
65 | -import(pgsql_util, [to_integer/1, to_atom/1]).
66 |
67 | -record(state, {options, ssl_options, transport,
68 | sasl_state,
69 | driver, params, socket, oidmap, as_binary}).
70 |
71 | start(Options) ->
72 | gen_server:start(?MODULE, [self(), Options], []).
73 |
74 | start_link(Options) ->
75 | gen_server:start_link(?MODULE, [self(), Options], []).
76 |
77 | init([DriverPid, Options]) ->
78 | %%io:format("Init~n", []),
79 | %% Default values: We connect to localhost on the standard TCP/IP
80 | %% port.
81 | {CommonOpts, SSLOpts} =
82 | lists:partition(
83 | fun({host, _}) -> true;
84 | ({port, _}) -> true;
85 | ({as_binary, _}) -> true;
86 | ({user, _}) -> true;
87 | ({password, _}) -> true;
88 | ({database, _}) -> true;
89 | ({transport, _}) -> true;
90 | ({connect_timeout, _}) -> true;
91 | (_) -> false
92 | end, Options),
93 | Host = option(CommonOpts, host, "localhost"),
94 | Port = option(CommonOpts, port, 5432),
95 | AsBinary = option(CommonOpts, as_binary, false),
96 | Transport = option(CommonOpts, transport, tcp),
97 | Timeout = option(CommonOpts, connect_timeout, 5000),
98 |
99 | case socket({Host, Port}, Timeout) of
100 | {ok, Sock} ->
101 | State = #state{options = CommonOpts,
102 | transport = Transport,
103 | ssl_options = SSLOpts,
104 | driver = DriverPid,
105 | as_binary = AsBinary,
106 | socket = Sock},
107 | case Transport of
108 | tcp -> connect(State);
109 | ssl -> starttls(State)
110 | end;
111 | Error ->
112 | Reason = {init, Error},
113 | {stop, Reason}
114 | end.
115 |
116 | starttls(StateData) ->
117 | SSLRequest = <>,
118 | PacketSize = 4 + size(SSLRequest),
119 | Sock = StateData#state.socket,
120 | ok = pgsql_util:send(Sock, <>),
121 | process_starttls(StateData).
122 |
123 | connect(StateData) ->
124 | %%io:format("Connect~n", []),
125 | %% Connection settings for database-login.
126 | %% TODO: Check if the default values are relevant:
127 | UserName = option(StateData#state.options, user, "cos"),
128 | DatabaseName = option(StateData#state.options, database, "template1"),
129 |
130 | %% Make protocol startup packet.
131 | Version = <>,
132 | User = make_pair(user, UserName),
133 | Database = make_pair(database, DatabaseName),
134 | StartupPacket = <>,
138 |
139 | %% Backend will continue with authentication after the startup packet
140 | PacketSize = 4 + size(StartupPacket),
141 | Sock = StateData#state.socket,
142 | ok = pgsql_util:send(Sock, <>),
143 | authenticate(StateData).
144 |
145 | process_starttls(StateData) ->
146 | Sock = StateData#state.socket,
147 | case recv_byte(Sock, 5000) of
148 | {ok, $S} ->
149 | case starttls(Sock, StateData#state.ssl_options) of
150 | {ok, Sock1} ->
151 | connect(StateData#state{socket = Sock1});
152 | {error, _} = Err ->
153 | throw(Err)
154 | end;
155 | {ok, $N} ->
156 | {stop, {starttls, denied}}
157 | end.
158 |
159 | authenticate(StateData) ->
160 | %% Await authentication request from backend.
161 | Sock = StateData#state.socket,
162 | AsBin = StateData#state.as_binary,
163 | {ok, Code, Packet} = recv_msg(Sock, 5000),
164 | {ok, Value} = decode_packet(Code, Packet, AsBin),
165 | case Value of
166 | %% Error response
167 | {error_message, Message} ->
168 | {stop, {authentication, Message}};
169 | {authenticate, {AuthMethod, AuthData}} ->
170 | case AuthMethod of
171 | 0 -> % Auth ok
172 | setup(StateData, []);
173 | 1 -> % Kerberos 4
174 | {stop, {nyi, auth_kerberos4}};
175 | 2 -> % Kerberos 5
176 | {stop, {nyi, auth_kerberos5}};
177 | 3 -> % Plaintext password
178 | Password = option(StateData#state.options, password, ""),
179 | EncodedPass = encode_message(pass_plain, Password),
180 | ok = send(Sock, EncodedPass),
181 | authenticate(StateData);
182 | 4 -> % Hashed password
183 | {stop, {nyi, auth_crypt}};
184 | 5 -> % MD5 password
185 | Password = option(StateData#state.options, password, ""),
186 | User = option(StateData#state.options, user, ""),
187 | EncodedPass = encode_message(pass_md5,
188 | {User, Password, AuthData}),
189 | ok = send(Sock, EncodedPass),
190 | authenticate(StateData);
191 | 10 -> % SASL
192 | Password = option(StateData#state.options, password, ""),
193 | User = option(StateData#state.options, user, ""),
194 | Mechs = binary:split(AuthData, <<0>>, [global, trim_all]),
195 | BUser = list_to_binary(User),
196 | BPassword = list_to_binary(Password),
197 | case pgsql_sasl:client_new(BUser, BPassword, Mechs) of
198 | {ok, Mech, Resp, SASLState} ->
199 | Response = encode_message(sasl_initial_response,
200 | {Mech, Resp}),
201 | ok = send(Sock, Response),
202 | authenticate(
203 | StateData#state{sasl_state = SASLState});
204 | {error, Error} ->
205 | {stop, {authentication, Error}}
206 | end;
207 | 11 -> % SASL continue
208 | case pgsql_sasl:client_step(StateData#state.sasl_state, AuthData) of
209 | {ok, Resp, SASLState} ->
210 | Response = encode_message(sasl_response, Resp),
211 | ok = send(Sock, Response),
212 | authenticate(
213 | StateData#state{sasl_state = SASLState});
214 | {error, Error} ->
215 | {stop, {authentication, Error}}
216 | end;
217 | 12 -> % SASL final
218 | case pgsql_sasl:client_finish(StateData#state.sasl_state, AuthData) of
219 | ok ->
220 | authenticate(
221 | StateData#state{sasl_state = undefined});
222 | {error, Error} ->
223 | {stop, {authentication, Error}}
224 | end;
225 | _ ->
226 | {stop, {authentication, {unknown, AuthMethod}}}
227 | end;
228 | %% Unknown message received
229 | Any ->
230 | {stop, {protocol_error, Any}}
231 | end.
232 |
233 | setup(StateData, Params) ->
234 | %% Receive startup messages until ReadyForQuery
235 | Sock = StateData#state.socket,
236 | AsBin = StateData#state.as_binary,
237 | {ok, Code, Package} = recv_msg(Sock, 5000),
238 | {ok, Pair} = decode_packet(Code, Package, AsBin),
239 | case Pair of
240 | %% BackendKeyData, necessary for issuing cancel requests
241 | {backend_key_data, {Pid, Secret}} ->
242 | Params1 = [{secret, {Pid, Secret}} | Params],
243 | setup(StateData, Params1);
244 | %% ParameterStatus, a key-value pair.
245 | {parameter_status, {Key, Value}} ->
246 | Params1 = [{{parameter, Key}, Value} | Params],
247 | setup(StateData, Params1);
248 | %% Error message, with a sequence of <>
249 | %% of error descriptions. Code==0 terminates the Reason.
250 | {error_message, Message} ->
251 | close(Sock),
252 | {stop, {error_response, Message}};
253 | %% Notice Response, with a sequence of <>
254 | %% identified fields. Code==0 terminates the Notice.
255 | {notice_response, Notice} ->
256 | deliver(StateData, {pgsql_notice, Notice}),
257 | setup(StateData, Params);
258 | %% Ready for Query, backend is ready for a new query cycle
259 | {ready_for_query, _Status} ->
260 | connected(StateData#state{params = Params}, Sock);
261 | Any ->
262 | {stop, {unknown_setup, Any}}
263 | end.
264 |
265 | %% Connected state. Can now start to push messages
266 | %% between frontend and backend. But first some setup.
267 | connected(StateData, Sock) ->
268 | %% Protocol unwrapping process. Factored out to make future
269 | %% SSL and unix domain support easier. Store process under
270 | %% 'socket' in the process dictionary.
271 | AsBin = StateData#state.as_binary,
272 | {ok, Unwrapper} = pgsql_socket:start_link(Sock, self(), AsBin),
273 | ok = controlling_process(Sock, Unwrapper),
274 |
275 | %% Lookup oid to type names and store them in a dictionary under
276 | %% 'oidmap' in the process dictionary.
277 | Packet = encode_message(squery, "SELECT oid, typname FROM pg_type"),
278 | ok = send(Sock, Packet),
279 | {ok, [{_, _ColDesc, Rows}]} = process_squery([], AsBin),
280 | Rows1 = lists:map(fun ([CodeS, NameS]) ->
281 | Code = to_integer(CodeS),
282 | Name = to_atom(NameS),
283 | {Code, Name}
284 | end,
285 | Rows),
286 | OidMap = dict:from_list(Rows1),
287 |
288 | {ok, StateData#state{oidmap = OidMap}}.
289 |
290 |
291 | handle_call(terminate, _From, State) ->
292 | Sock = State#state.socket,
293 | Packet = encode_message(terminate, []),
294 | ok = send(Sock, Packet),
295 | close(Sock),
296 | Reply = ok,
297 | {stop, normal, Reply, State};
298 |
299 | %% Simple query
300 | handle_call({squery, Query}, _From, State) ->
301 | Sock = State#state.socket,
302 | AsBin = State#state.as_binary,
303 | Packet = encode_message(squery, Query),
304 | ok = send(Sock, Packet),
305 | {ok, Result} = process_squery([], AsBin),
306 | case lists:keymember(error, 1, Result) of
307 | true ->
308 | RBPacket = encode_message(squery, "ROLLBACK"),
309 | ok = send(Sock, RBPacket),
310 | {ok, _RBResult} = process_squery([], AsBin);
311 | _ ->
312 | ok
313 | end,
314 | Reply = {ok, Result},
315 | {reply, Reply, State};
316 |
317 | %% Extended query
318 | %% simplistic version using the unnammed prepared statement and portal.
319 | handle_call({equery, {Query, Params}}, _From, State) ->
320 | Sock = State#state.socket,
321 | ParseP = encode_message(parse, {"", Query, []}),
322 | BindP = encode_message(bind, {"", "", Params, [binary]}),
323 | DescribeP = encode_message(describe, {portal, ""}),
324 | ExecuteP = encode_message(execute, {"", 0}),
325 | SyncP = encode_message(sync, []),
326 | ok = send(Sock, [ParseP, BindP, DescribeP, ExecuteP, SyncP]),
327 |
328 | {ok, Command, Desc, Status, Logs} = process_equery(State, []),
329 |
330 | OidMap = State#state.oidmap,
331 | NameTypes = lists:map(fun({Name, _Format, _ColNo, Oid, _, _, _}) ->
332 | {Name, dict:fetch(Oid, OidMap)}
333 | end,
334 | Desc),
335 | Reply = {ok, Command, Status, NameTypes, Logs},
336 | {reply, Reply, State};
337 |
338 | %% Prepare a statement, so it can be used for queries later on.
339 | handle_call({prepare, {Name, Query}}, _From, State) ->
340 | Sock = State#state.socket,
341 | %{value, {TextOid, _}} =
342 | % lists:keysearch(text, 2, dict:to_list(State#state.oidmap)),
343 | %ParamOids = [TextOid || _ <- lists:seq(1, 10)],
344 | send_message(Sock, parse, {Name, Query, []}),
345 | send_message(Sock, describe, {prepared_statement, Name}),
346 | send_message(Sock, sync, []),
347 | case process_prepare({[], []}) of
348 | {ok, Status, ParamDesc, ResultDesc} ->
349 | OidMap = State#state.oidmap,
350 | ParamTypes =
351 | lists:map(fun (Oid) -> dict:fetch(Oid, OidMap) end, ParamDesc),
352 | ResultNameTypes = lists:map(fun ({ColName, _Format, _ColNo, Oid, _, _, _}) ->
353 | {ColName, dict:fetch(Oid, OidMap)}
354 | end,
355 | ResultDesc),
356 | Reply = {ok, Status, ParamTypes, ResultNameTypes},
357 | {reply, Reply, State};
358 | {error, Error} ->
359 | Reply = {error, Error},
360 | {reply, Reply, State}
361 | end;
362 |
363 | %% Close a prepared statement.
364 | handle_call({unprepare, Name}, _From, State) ->
365 | Sock = State#state.socket,
366 | send_message(Sock, close, {prepared_statement, Name}),
367 | send_message(Sock, sync, []),
368 | {ok, _Status} = process_unprepare(),
369 | Reply = ok,
370 | {reply, Reply, State};
371 |
372 | %% Execute a prepared statement
373 | handle_call({execute, {Name, Params}}, _From, State) ->
374 | Sock = State#state.socket,
375 | %%io:format("execute: ~p ~p ~n", [Name, Params]),
376 | begin % Issue first requests for the prepared statement.
377 | BindP = encode_message(bind, {"", Name, Params, [binary]}),
378 | DescribeP = encode_message(describe, {portal, ""}),
379 | ExecuteP = encode_message(execute, {"", 0}),
380 | FlushP = encode_message(flush, []),
381 | ok = send(Sock, [BindP, DescribeP, ExecuteP, FlushP])
382 | end,
383 | receive
384 | {pgsql, {bind_complete, _}} -> % Bind reply first.
385 | %% Collect response to describe message,
386 | %% which gives a hint of the rest of the messages.
387 | case process_execute(State, Sock) of
388 | {ok, Command, Result} ->
389 |
390 | begin % Close portal and end extended query.
391 | CloseP = encode_message(close, {portal, ""}),
392 | SyncP = encode_message(sync, []),
393 | ok = send(Sock, [CloseP, SyncP])
394 | end,
395 | receive
396 | %% Collect response to close message.
397 | {pgsql, {close_complete, _}} ->
398 | receive
399 | %% Collect response to sync message.
400 | {pgsql, {ready_for_query, _Status}} ->
401 | %%io:format("execute: ~p ~p ~p~n",
402 | %% [Status, Command, Result]),
403 | Reply = {ok, {Command, Result}},
404 | {reply, Reply, State};
405 | {pgsql, Unknown} ->
406 | {stop, Unknown, {error, Unknown}, State}
407 | end;
408 | {pgsql, Unknown} ->
409 | {stop, Unknown, {error, Unknown}, State}
410 | end;
411 | {error, _} = Error ->
412 | begin
413 | SyncP = encode_message(sync, []),
414 | ok = send(Sock, [SyncP])
415 | end,
416 | receive
417 | {pgsql, {ready_for_query, _Status}} ->
418 | Reply = Error,
419 | {reply, Reply, State};
420 | {pgsql, Unknown} ->
421 | {stop, Unknown, {error, Unknown}, State}
422 | end
423 | end;
424 | {pgsql, {error_message, Error}} ->
425 | begin
426 | SyncP = encode_message(sync, []),
427 | ok = send(Sock, [SyncP])
428 | end,
429 | receive
430 | %% Collect response to sync message.
431 | {pgsql, {ready_for_query, _Status}} ->
432 | %%io:format("execute: ~p ~p ~p~n",
433 | %% [Status, Command, Result]),
434 | Reply = {error, Error},
435 | {reply, Reply, State};
436 | {pgsql, Unknown} ->
437 | {stop, Unknown, {error, Unknown}, State}
438 | end;
439 | {pgsql, Unknown} ->
440 | {stop, Unknown, {error, Unknown}, State}
441 | end;
442 |
443 | handle_call(_Request, _From, State) ->
444 | Reply = ok,
445 | {reply, Reply, State}.
446 |
447 |
448 | handle_cast(_Msg, State) ->
449 | {noreply, State}.
450 |
451 |
452 | code_change(_OldVsn, State, _Extra) ->
453 | {ok, State}.
454 |
455 |
456 | %% Socket closed or socket error messages.
457 | handle_info({socket, _Sock, Condition}, State) ->
458 | {stop, {socket, Condition}, State};
459 | handle_info(_Info, State) ->
460 | %%io:format("pgsql unexpected info ~p~n", [_Info]),
461 | {noreply, State}.
462 |
463 |
464 | terminate(_Reason, _State) ->
465 | ok.
466 |
467 |
468 | deliver(State, Message) ->
469 | DriverPid = State#state.driver,
470 | DriverPid ! Message.
471 |
472 | %% In the process_squery state we collect responses until the backend is
473 | %% done processing.
474 | process_squery(Log, AsBin) ->
475 | receive
476 | {pgsql, {row_description, Cols}} ->
477 | case process_squery_cols([], AsBin) of
478 | {ok, Command, Rows} ->
479 | process_squery([{Command, Cols, Rows}|Log], AsBin);
480 | {error, _} = Error ->
481 | process_squery([Error | Log], AsBin)
482 | end;
483 | {pgsql, {command_complete, Command}} ->
484 | process_squery([Command|Log], AsBin);
485 | {pgsql, {ready_for_query, _Status}} ->
486 | {ok, lists:reverse(Log)};
487 | {pgsql, {error_message, Error}} ->
488 | process_squery([{error, Error}|Log], AsBin);
489 | {pgsql, _Any} ->
490 | process_squery(Log, AsBin)
491 | end.
492 | process_squery_cols(Log, AsBin) ->
493 | receive
494 | {pgsql, {data_row, Row}} ->
495 | process_squery_cols(
496 | [lists:map(
497 | fun(null) ->
498 | null;
499 | (R) when AsBin == true ->
500 | R;
501 | (R) ->
502 | binary_to_list(R)
503 | end, Row) | Log], AsBin);
504 | {pgsql, {command_complete, Command}} ->
505 | {ok, Command, lists:reverse(Log)};
506 | {pgsql, {error_message, Error}} ->
507 | {error, Error};
508 | {pgsql, _Any} ->
509 | process_squery_cols(Log, AsBin)
510 | end.
511 |
512 | process_equery(State, Log) ->
513 | receive
514 | %% Consume parse and bind complete messages when waiting for the first
515 | %% first row_description message. What happens if the equery doesnt
516 | %% return a result set?
517 | {pgsql, {parse_complete, _}} ->
518 | process_equery(State, Log);
519 | {pgsql, {bind_complete, _}} ->
520 | process_equery(State, Log);
521 | {pgsql, {row_description, Descs}} ->
522 | OidMap = State#state.oidmap,
523 | AsBin = State#state.as_binary,
524 | {ok, Descs1} = pgsql_util:decode_descs(OidMap, Descs),
525 | process_equery_datarow(Descs1, Log, {undefined, Descs, undefined},
526 | AsBin);
527 | {pgsql, Any} ->
528 | process_equery(State, [Any|Log])
529 | end.
530 |
531 | process_equery_datarow(Types, Log, Info={Command, Desc, Status}, AsBin) ->
532 | receive
533 | %%
534 | {pgsql, {command_complete, Command1}} ->
535 | process_equery_datarow(Types, Log, {Command1, Desc, Status}, AsBin);
536 | {pgsql, {ready_for_query, Status1}} ->
537 | {ok, Command, Desc, Status1, lists:reverse(Log)};
538 | {pgsql, {data_row, Row}} ->
539 | {ok, DecodedRow} = pgsql_util:decode_row(Types, Row, AsBin),
540 | process_equery_datarow(Types, [DecodedRow|Log], Info, AsBin);
541 | {pgsql, Any} ->
542 | process_equery_datarow(Types, [Any|Log], Info, AsBin)
543 | end.
544 |
545 | process_prepare(Info={ParamDesc, ResultDesc}) ->
546 | receive
547 | {pgsql, {no_data, _}} ->
548 | process_prepare({ParamDesc, []});
549 | {pgsql, {parse_complete, _}} ->
550 | process_prepare(Info);
551 | {pgsql, {parameter_description, Oids}} ->
552 | process_prepare({Oids, ResultDesc});
553 | {pgsql, {row_description, Desc}} ->
554 | process_prepare({ParamDesc, Desc});
555 | {pgsql, {ready_for_query, Status}} ->
556 | {ok, Status, ParamDesc, ResultDesc};
557 | {pgsql, {error_message, Error}} ->
558 | process_prepare_error(Error);
559 | {pgsql, _Any} ->
560 | %%io:format("process_prepare: ~p~n", [Any]),
561 | process_prepare(Info)
562 | end.
563 |
564 | process_prepare_error(Error) ->
565 | receive
566 | {pgsql, {ready_for_query, _Status}} ->
567 | {error, Error};
568 | {pgsql, _Any} ->
569 | %%io:format("process_prepare: ~p~n", [Any]),
570 | process_prepare_error(Error)
571 | end.
572 |
573 | process_unprepare() ->
574 | receive
575 | {pgsql, {ready_for_query, Status}} ->
576 | {ok, Status};
577 | {pgsql, {close_complate, []}} ->
578 | process_unprepare();
579 | {pgsql, _Any} ->
580 | %%io:format("process_unprepare: ~p~n", [Any]),
581 | process_unprepare()
582 | end.
583 |
584 | process_execute(State, Sock) ->
585 | %% Either the response begins with a no_data or a row_description
586 | %% Needs to return {ok, Status, Result}
587 | %% where Result = {Command, ...}
588 | receive
589 | {pgsql, {no_data, _}} ->
590 | process_execute_nodata();
591 | {pgsql, {row_description, Descs}} ->
592 | OidMap = State#state.oidmap,
593 | AsBin = State#state.as_binary,
594 | {ok, Types} = pgsql_util:decode_descs(OidMap, Descs),
595 | process_execute_resultset(Sock, Types, [], AsBin);
596 | {pgsql, Unknown} ->
597 | exit(Unknown)
598 | end.
599 |
600 | process_execute_nodata() ->
601 | receive
602 | {pgsql, {command_complete, Cmd}} ->
603 | Command = if is_binary(Cmd) ->
604 | binary_to_list(Cmd);
605 | true ->
606 | Cmd
607 | end,
608 | case Command of
609 | "INSERT "++Rest ->
610 | {ok, [{integer, _, _Table},
611 | {integer, _, NRows}], _} = erl_scan:string(Rest),
612 | {ok, 'INSERT', NRows};
613 | "SELECT" ->
614 | {ok, 'SELECT', should_not_happen};
615 | "DELETE "++Rest ->
616 | {ok, [{integer, _, NRows}], _} =
617 | erl_scan:string(Rest),
618 | {ok, 'DELETE', NRows};
619 | "UPDATE "++Rest ->
620 | {ok, [{integer, _, NRows}], _} =
621 | erl_scan:string(Rest),
622 | {ok, 'UPDATE', NRows};
623 | Any ->
624 | {ok, nyi, Any}
625 | end;
626 |
627 | {pgsql, {error_message, Error}} ->
628 | {error, Error};
629 | {pgsql, Unknown} ->
630 | exit(Unknown)
631 | end.
632 | process_execute_resultset(Sock, Types, Log, AsBin) ->
633 | receive
634 | {pgsql, {command_complete, Command}} ->
635 | {ok, Command, lists:reverse(Log)};
636 | {pgsql, {data_row, Row}} ->
637 | {ok, DecodedRow} = pgsql_util:decode_row(Types, Row, AsBin),
638 | process_execute_resultset(Sock, Types, [DecodedRow|Log], AsBin);
639 | {pgsql, {portal_suspended, _}} ->
640 | throw(portal_suspended);
641 | {pgsql, {error_message, Error}} ->
642 | {error, Error};
643 | {pgsql, Any} ->
644 | %%process_execute_resultset(Types, [Any|Log])
645 | exit(Any)
646 | end.
647 |
648 | %% With a message type Code and the payload Packet apropriate
649 | %% decoding procedure can proceed.
650 | decode_packet(Code, Packet, AsBin) ->
651 | Ret = fun(CodeName, Values) -> {ok, {CodeName, Values}} end,
652 | case Code of
653 | ?PG_ERROR_MESSAGE ->
654 | Message = pgsql_util:errordesc(Packet, AsBin),
655 | Ret(error_message, Message);
656 | ?PG_EMPTY_RESPONSE ->
657 | Ret(empty_response, []);
658 | ?PG_ROW_DESCRIPTION ->
659 | <<_Columns:16/integer, ColDescs/binary>> = Packet,
660 | Descs = coldescs(ColDescs, [], AsBin),
661 | Ret(row_description, Descs);
662 | ?PG_READY_FOR_QUERY ->
663 | <> = Packet,
664 | case State of
665 | $I ->
666 | Ret(ready_for_query, idle);
667 | $T ->
668 | Ret(ready_for_query, transaction);
669 | $E ->
670 | Ret(ready_for_query, failed_transaction)
671 | end;
672 | ?PG_COMMAND_COMPLETE ->
673 | {Task, _} = to_string(Packet, AsBin),
674 | Ret(command_complete, Task);
675 | ?PG_DATA_ROW ->
676 | <> = Packet,
677 | ColData = datacoldescs(NumberCol, RowData, []),
678 | Ret(data_row, ColData);
679 | ?PG_BACKEND_KEY_DATA ->
680 | <> = Packet,
681 | Ret(backend_key_data, {Pid, Secret});
682 | ?PG_PARAMETER_STATUS ->
683 | {Key, Value} = split_pair(Packet, AsBin),
684 | Ret(parameter_status, {Key, Value});
685 | ?PG_NOTICE_RESPONSE ->
686 | Ret(notice_response, []);
687 | ?PG_AUTHENTICATE ->
688 | <> = Packet,
689 | Ret(authenticate, {AuthMethod, Salt});
690 | ?PG_PARSE_COMPLETE ->
691 | Ret(parse_complete, []);
692 | ?PG_BIND_COMPLETE ->
693 | Ret(bind_complete, []);
694 | ?PG_PORTAL_SUSPENDED ->
695 | Ret(portal_suspended, []);
696 | ?PG_CLOSE_COMPLETE ->
697 | Ret(close_complete, []);
698 | $t ->
699 | <<_NParams:16/integer, OidsP/binary>> = Packet,
700 | Oids = pgsql_util:oids(OidsP, []),
701 | Ret(parameter_description, Oids);
702 | ?PG_NO_DATA ->
703 | Ret(no_data, []);
704 | _Any ->
705 | Ret(unknown, [Code])
706 | end.
707 |
708 | send_message(Sock, Type, Values) ->
709 | %%io:format("send_message:~p~n", [{Type, Values}]),
710 | Packet = encode_message(Type, Values),
711 | ok = send(Sock, Packet).
712 |
713 | %% Add header to a message.
714 | encode(Code, Packet) ->
715 | Len = size(Packet) + 4,
716 | <>.
717 |
718 | %% Encode a message of a given type.
719 | encode_message(pass_plain, Password) ->
720 | Pass = pgsql_util:pass_plain(Password),
721 | encode($p, Pass);
722 | encode_message(pass_md5, {User, Password, Salt}) ->
723 | Pass = pgsql_util:pass_md5(User, Password, Salt),
724 | encode($p, Pass);
725 | encode_message(sasl_initial_response, {Mech, Resp}) ->
726 | RespLen = size(Resp),
727 | Msg = <>,
728 | encode($p, Msg);
729 | encode_message(sasl_response, Resp) ->
730 | encode($p, Resp);
731 | encode_message(terminate, _) ->
732 | encode($X, <<>>);
733 | encode_message(squery, Query) -> % squery as in simple query.
734 | encode($Q, string(Query));
735 | encode_message(close, {Object, Name}) ->
736 | Type = case Object of prepared_statement -> $S; portal -> $P end,
737 | String = string(Name),
738 | encode($C, <>);
739 | encode_message(describe, {Object, Name}) ->
740 | ObjectP = case Object of prepared_statement -> $S; portal -> $P end,
741 | NameP = string(Name),
742 | encode($D, <>);
743 | encode_message(flush, _) ->
744 | encode($H, <<>>);
745 | encode_message(parse, {Name, Query, Oids}) ->
746 | StringName = string(Name),
747 | StringQuery = string(Query),
748 | Params = length(Oids),
749 | Types = iolist_to_binary([<> || Oid <- Oids]),
750 | encode($P, <>);
752 | encode_message(bind, {NamePortal, NamePrepared,
753 | Parameters, ResultFormats}) ->
754 | PortalP = string(NamePortal),
755 | PreparedP = string(NamePrepared),
756 |
757 | ParamFormatsList = lists:map(
758 | fun (Bin) when is_binary(Bin) -> <<1:16/integer>>;
759 | (_Text) -> <<0:16/integer>> end,
760 | Parameters),
761 | ParamFormatsP = erlang:list_to_binary(ParamFormatsList),
762 |
763 | NParameters = length(Parameters),
764 | ParametersList = lists:map(
765 | fun (null) ->
766 | Minus = -1,
767 | <>;
768 | (Bin) when is_binary(Bin) ->
769 | Size = size(Bin),
770 | <>;
771 | (Integer) when is_integer(Integer) ->
772 | List = integer_to_list(Integer),
773 | Bin = list_to_binary(List),
774 | Size = size(Bin),
775 | <>;
776 | (Text) ->
777 | Bin = list_to_binary(Text),
778 | Size = size(Bin),
779 | <>
780 | end,
781 | Parameters),
782 | ParametersP = erlang:list_to_binary(ParametersList),
783 |
784 | NResultFormats = length(ResultFormats),
785 | ResultFormatsList = lists:map(
786 | fun (binary) -> <<1:16/integer>>;
787 | (text) -> <<0:16/integer>> end,
788 | ResultFormats),
789 | ResultFormatsP = erlang:list_to_binary(ResultFormatsList),
790 |
791 | %%io:format("encode bind: ~p~n", [{PortalP, PreparedP,
792 | %% NParameters, ParamFormatsP,
793 | %% NParameters, ParametersP,
794 | %% NResultFormats, ResultFormatsP}]),
795 | encode($B, <>);
799 | encode_message(execute, {Portal, Limit}) ->
800 | String = string(Portal),
801 | encode($E, <>);
802 | encode_message(sync, _) ->
803 | encode($S, <<>>).
804 |
--------------------------------------------------------------------------------
/src/pgsql_sasl.erl:
--------------------------------------------------------------------------------
1 | %%%-------------------------------------------------------------------
2 | %%% @author Alexey Shchepin
3 | %%%
4 | %%% Copyright (C) 2002-2025 ProcessOne, SARL. All Rights Reserved.
5 | %%%
6 | %%% Licensed under the Apache License, Version 2.0 (the "License");
7 | %%% you may not use this file except in compliance with the License.
8 | %%% You may obtain a copy of the License at
9 | %%%
10 | %%% http://www.apache.org/licenses/LICENSE-2.0
11 | %%%
12 | %%% Unless required by applicable law or agreed to in writing, software
13 | %%% distributed under the License is distributed on an "AS IS" BASIS,
14 | %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | %%% See the License for the specific language governing permissions and
16 | %%% limitations under the License.
17 | %%%
18 | %%%-------------------------------------------------------------------
19 | -module(pgsql_sasl).
20 | -author('alexey@process-one.net').
21 |
22 | -export([client_new/3, client_step/2, client_finish/2]).
23 |
24 | -record(sasl_state,
25 | {user :: binary(),
26 | password :: binary(),
27 | nonce :: binary(),
28 | verify = <<>> :: binary()
29 | }).
30 |
31 | -spec client_new(binary(), binary(), list(binary())) ->
32 | {ok, binary(), binary(), #sasl_state{}} |
33 | {error, any()}.
34 | client_new(User, Password, Mechs) ->
35 | case lists:member(<<"SCRAM-SHA-256">>, Mechs) of
36 | true ->
37 | Nonce = base64:encode(crypto:strong_rand_bytes(18)),
38 | State = #sasl_state{user = User,
39 | password = Password,
40 | nonce = Nonce},
41 | Msg = client_first_message_bare(User, Nonce),
42 | Response = <<"n,,", Msg/binary>>,
43 | {ok, <<"SCRAM-SHA-256">>, Response, State};
44 | false ->
45 | {error, "No supported SASL mechs"}
46 | end.
47 |
48 | -spec client_step(#sasl_state{}, binary()) ->
49 | {ok, binary(), #sasl_state{}} |
50 | {error, any()}.
51 | client_step(State, ServerResponse) ->
52 | case parse(ServerResponse) of
53 | SResp when is_list(SResp) ->
54 | I = binary_to_integer(proplists:get_value(<<"i">>, SResp)),
55 | R = proplists:get_value(<<"r">>, SResp),
56 | S = base64:decode(proplists:get_value(<<"s">>, SResp)),
57 | Nonce = State#sasl_state.nonce,
58 | NonceSize = size(Nonce),
59 | case R of
60 | <> ->
61 | ClientMsg1 = client_first_message_bare(
62 | State#sasl_state.user,
63 | State#sasl_state.nonce),
64 | ClientMsg2 = <<"c=biws,r=", R/binary>>,
65 | AuthMessage =
66 | <>,
69 | Password = State#sasl_state.password,
70 | SaltedPassword = scram:salted_password(
71 | sha256, Password, S, I),
72 | ClientKey =
73 | scram:client_key(sha256, SaltedPassword),
74 | StoredKey = scram:stored_key(sha256, ClientKey),
75 | ClientSignature =
76 | scram:client_signature(sha256, StoredKey, AuthMessage),
77 | ClientProof =
78 | crypto:exor(ClientKey, ClientSignature),
79 | P = base64:encode(ClientProof),
80 | Msg = <>,
81 | ServerKey =
82 | scram:server_key(sha256, SaltedPassword),
83 | V = scram:server_signature(sha256, ServerKey, AuthMessage),
84 | {ok, Msg, State#sasl_state{nonce = R, verify = V}};
85 | _ ->
86 | {error, "Bad SASL server nonce"}
87 | end;
88 | _ ->
89 | {error, {"Error parsing server response", ServerResponse}}
90 | end.
91 |
92 | -spec client_finish(#sasl_state{}, binary()) -> ok | {error, any()}.
93 | client_finish(State, ServerResponse) ->
94 | case parse(ServerResponse) of
95 | SResp when is_list(SResp) ->
96 | V = base64:decode(proplists:get_value(<<"v">>, SResp)),
97 | if
98 | State#sasl_state.verify == V ->
99 | ok;
100 | true ->
101 | {error, "SASL server verification failed"}
102 | end;
103 | _ ->
104 | {error, {"Error parsing server response", ServerResponse}}
105 | end.
106 |
107 | client_first_message_bare(User, Nonce) ->
108 | <<"n=", User/binary, ",r=", Nonce/binary>>.
109 |
110 | parse(S) -> parse1(S, <<>>, []).
111 |
112 | parse1(<<$=, Cs/binary>>, S, Ts) ->
113 | parse2(Cs, S, <<>>, Ts);
114 | parse1(<<$,, Cs/binary>>, <<>>, Ts) -> parse1(Cs, <<>>, Ts);
115 | parse1(<<$\s, Cs/binary>>, <<>>, Ts) -> parse1(Cs, <<>>, Ts);
116 | parse1(<>, S, Ts) -> parse1(Cs, <>, Ts);
117 | parse1(<<>>, <<>>, T) -> lists:reverse(T);
118 | parse1(<<>>, _S, _T) -> bad.
119 |
120 | parse2(<<$", Cs/binary>>, Key, Val, Ts) ->
121 | parse3(Cs, Key, Val, Ts);
122 | parse2(<>, Key, Val, Ts) ->
123 | parse4(Cs, Key, <>, Ts);
124 | parse2(<<>>, _, _, _) -> bad.
125 |
126 | parse3(<<$", Cs/binary>>, Key, Val, Ts) ->
127 | parse4(Cs, Key, Val, Ts);
128 | parse3(<<$\\, C, Cs/binary>>, Key, Val, Ts) ->
129 | parse3(Cs, Key, <>, Ts);
130 | parse3(<>, Key, Val, Ts) ->
131 | parse3(Cs, Key, <>, Ts);
132 | parse3(<<>>, _, _, _) -> bad.
133 |
134 | parse4(<<$,, Cs/binary>>, Key, Val, Ts) ->
135 | parse1(Cs, <<>>, [{Key, Val} | Ts]);
136 | parse4(<<$\s, Cs/binary>>, Key, Val, Ts) ->
137 | parse4(Cs, Key, Val, Ts);
138 | parse4(<>, Key, Val, Ts) ->
139 | parse4(Cs, Key, <>, Ts);
140 | parse4(<<>>, Key, Val, Ts) ->
141 | parse1(<<>>, <<>>, [{Key, Val} | Ts]).
142 |
143 |
--------------------------------------------------------------------------------
/src/pgsql_socket.erl:
--------------------------------------------------------------------------------
1 | %%% File : pgsql_socket.erl
2 | %%% Author : Blah
3 | %%% Description : Unwrapping of TCP/SSL line protocol packages to postgres messages.
4 | %%% Created : 22 Jul 2005
5 |
6 | -module(pgsql_socket).
7 |
8 | -behaviour(gen_server).
9 |
10 | -export([start/3, start_link/3]).
11 |
12 | %% gen_server callbacks
13 | -export([init/1,
14 | handle_call/3,
15 | handle_cast/2,
16 | code_change/3,
17 | handle_info/2,
18 | terminate/2]).
19 |
20 | -record(state, {socket, sockmod, protopid, buffer, as_binary}).
21 |
22 | start(Sock, ProtoPid, AsBin) ->
23 | gen_server:start(?MODULE, [Sock, ProtoPid, AsBin], []).
24 |
25 | start_link(Sock, ProtoPid, AsBin) ->
26 | gen_server:start_link(?MODULE, [Sock, ProtoPid, AsBin], []).
27 |
28 | init([{Mod, Sock}, ProtoPid, AsBin]) ->
29 | setopts({Mod, Sock}, [{active, once}]),
30 | {ok, #state{socket = Sock, sockmod = Mod, protopid = ProtoPid,
31 | buffer = <<>>, as_binary = AsBin}}.
32 |
33 | handle_call(_Request, _From, State) ->
34 | Reply = ok,
35 | {reply, Reply, State}.
36 |
37 | handle_cast(_Msg, State) ->
38 | {noreply, State}.
39 |
40 | code_change(_OldVsn, State, _Extra) ->
41 | {ok, State}.
42 |
43 | handle_info({Tag, Sock, Bin},
44 | #state{socket = Sock,
45 | sockmod = Mod,
46 | protopid = ProtoPid,
47 | as_binary = AsBin,
48 | buffer = Buffer} = State)
49 | when Tag == tcp; Tag == ssl ->
50 | {ok, Rest} = process_buffer(ProtoPid, AsBin, <>),
51 | setopts({Mod, Sock}, [{active, once}]),
52 | {noreply, State#state{buffer = Rest}};
53 | handle_info({Tag, Sock},
54 | #state{socket = Sock, sockmod = Mod,
55 | protopid = ProtoPid} = State)
56 | when Tag == tcp_closed; Tag == ssl_closed ->
57 | io:format("Sock closed~n", []),
58 | ProtoPid ! {socket, {Mod, Sock}, closed},
59 | {stop, tcp_close, State};
60 | handle_info({Tag, Sock, Reason},
61 | #state{socket = Sock, sockmod = Mod,
62 | protopid = ProtoPid} = State)
63 | when Tag == tcp_error; Tag == ssl_error ->
64 | io:format("Sock error~n", []),
65 | ProtoPid ! {socket, {Mod, Sock}, {error, Reason}},
66 | {stop, tcp_error, State};
67 | handle_info(_Info, State) ->
68 | {noreply, State}.
69 |
70 |
71 | terminate(_Reason, _State) ->
72 | ok.
73 |
74 |
75 | %% Given a binary that begins with a proper message header the binary
76 | %% will be processed for each full message it contains, and it will
77 | %% return any trailing incomplete messages.
78 | process_buffer(ProtoPid, AsBin,
79 | Bin = <>) ->
80 | Payload = Size - 4,
81 | if
82 | size(Rest) >= Payload ->
83 | <> = Rest,
84 | {ok, Message} = pgsql_proto:decode_packet(Code, Packet, AsBin),
85 | ProtoPid ! {pgsql, Message},
86 | process_buffer(ProtoPid, AsBin, Rest1);
87 | true ->
88 | {ok, Bin}
89 | end;
90 | process_buffer(_ProtoPid, _AsBin, Bin) when is_binary(Bin) ->
91 | {ok, Bin}.
92 |
93 | setopts({gen_tcp, Sock}, Opts) ->
94 | inet:setopts(Sock, Opts);
95 | setopts({ssl, Sock}, Opts) ->
96 | ssl:setopts(Sock, Opts).
97 |
--------------------------------------------------------------------------------
/src/pgsql_sup.erl:
--------------------------------------------------------------------------------
1 | %%%----------------------------------------------------------------------
2 | %%% File : pgsql_sup.erl
3 | %%% Author : Evgeniy Khramtsov
4 | %%% Purpose : PostgreSQL erlang driver supervisor
5 | %%% Created : 15 May 2013 by Evgeniy Khramtsov
6 | %%%
7 | %%%
8 | %%% p1_pgsql, Copyright (C) 2002-2025 ProcessOne
9 | %%%
10 | %%% This program is free software; you can redistribute it and/or
11 | %%% modify it under the terms of the GNU General Public License as
12 | %%% published by the Free Software Foundation; either version 2 of the
13 | %%% License, or (at your option) any later version.
14 | %%%
15 | %%% This program is distributed in the hope that it will be useful,
16 | %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 | %%% General Public License for more details.
19 | %%%
20 | %%% You should have received a copy of the GNU General Public License
21 | %%% along with this program; if not, write to the Free Software
22 | %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
23 | %%% 02111-1307 USA
24 | %%%
25 | %%%----------------------------------------------------------------------
26 |
27 | -module(pgsql_sup).
28 |
29 | -behaviour(supervisor).
30 |
31 | %% API
32 | -export([start_link/0]).
33 |
34 | %% Supervisor callbacks
35 | -export([init/1]).
36 |
37 | -define(SERVER, ?MODULE).
38 |
39 | %%%===================================================================
40 | %%% API functions
41 | %%%===================================================================
42 | start_link() ->
43 | supervisor:start_link({local, ?SERVER}, ?MODULE, []).
44 |
45 | %%%===================================================================
46 | %%% Supervisor callbacks
47 | %%%===================================================================
48 | init([]) ->
49 | {ok, {{one_for_one, 10, 1}, []}}.
50 |
51 | %%%===================================================================
52 | %%% Internal functions
53 | %%%===================================================================
54 |
--------------------------------------------------------------------------------
/src/pgsql_util.erl:
--------------------------------------------------------------------------------
1 | %%% File : pgsql_util.erl
2 | %%% Author : Christian Sunesson
3 | %%% Description : utility functions used in implementation of
4 | %%% postgresql driver.
5 | %%% Created : 11 May 2005 by Blah
6 |
7 | -module(pgsql_util).
8 |
9 | %% Key-Value handling
10 | -export([option/3]).
11 |
12 | %% Networking
13 | -export([socket/2, close/1, controlling_process/2, starttls/2]).
14 | -export([send/2, send_int/2, send_msg/3]).
15 | -export([recv_msg/2, recv_msg/1, recv_byte/2, recv_byte/1]).
16 |
17 | %% Protocol packing
18 | -export([string/1, make_pair/2, split_pair/2]).
19 | -export([split_pair_rec/2]).
20 | -export([count_string/1, to_string/2]).
21 | -export([oids/2, coldescs/3, datacoldescs/3]).
22 | -export([decode_row/3, decode_descs/2]).
23 | -export([errordesc/2]).
24 | -export([to_integer/1, to_atom/1]).
25 |
26 | -export([zip/2]).
27 |
28 | %% Constructing authentication messages.
29 | -export([pass_plain/1, pass_md5/3]).
30 | -import(erlang, [md5/1]).
31 | -export([hexlist/2]).
32 | -include_lib("kernel/include/inet.hrl").
33 |
34 | %% Lookup key in a plist stored in process dictionary under 'options'.
35 | %% Default is returned if there is no value for Key in the plist.
36 | option(Opts, Key, Default) ->
37 | case proplists:get_value(Key, Opts, Default) of
38 | Default ->
39 | Default;
40 | Value when is_binary(Value) ->
41 | binary_to_list(Value);
42 | Value ->
43 | Value
44 | end.
45 |
46 |
47 | %% Open a connection
48 | socket({Host, Port}, Timeout) ->
49 | case connect(Host, Port, Timeout) of
50 | {ok, Sock} ->
51 | {ok, {gen_tcp, Sock}};
52 | {error, _} = Err ->
53 | Err
54 | end.
55 |
56 | close({Mod, Sock}) ->
57 | Mod:close(Sock).
58 |
59 | controlling_process({Mod, Sock}, Owner) ->
60 | Mod:controlling_process(Sock, Owner).
61 |
62 | starttls({gen_tcp, Sock}, Opts) ->
63 | inet:setopts(Sock, [{active, once}]),
64 | case ssl:connect(Sock, Opts, 5000) of
65 | {ok, SSLSock} ->
66 | ssl:setopts(SSLSock, [{active, false}]),
67 | {ok, {ssl, SSLSock}};
68 | {error, _} = Err ->
69 | Err
70 | end.
71 |
72 | send({Mod, Sock}, Packet) ->
73 | Mod:send(Sock, Packet).
74 | send_int({Mod, Sock}, Int) ->
75 | Packet = <>,
76 | Mod:send(Sock, Packet).
77 |
78 | send_msg({Mod, Sock}, Code, Packet) when is_binary(Packet) ->
79 | Len = size(Packet) + 4,
80 | Msg = <>,
81 | Mod:send(Sock, Msg).
82 |
83 | recv_msg({Mod, Sock}, Timeout) ->
84 | {ok, Head} = Mod:recv(Sock, 5, Timeout),
85 | <> = Head,
86 | %%io:format("Code: ~p, Size: ~p~n", [Code, Size]),
87 | if
88 | Size > 4 ->
89 | {ok, Packet} = Mod:recv(Sock, Size-4, Timeout),
90 | {ok, Code, Packet};
91 | true ->
92 | {ok, Code, <<>>}
93 | end.
94 | recv_msg({Mod, Sock}) ->
95 | recv_msg({Mod, Sock}, infinity).
96 |
97 |
98 | recv_byte({Mod, Sock}) ->
99 | recv_byte({Mod, Sock}, infinity).
100 | recv_byte({Mod, Sock}, Timeout) ->
101 | case Mod:recv(Sock, 1, Timeout) of
102 | {ok, <>} ->
103 | {ok, Byte};
104 | E={error, _Reason} ->
105 | throw(E)
106 | end.
107 |
108 | %% Convert String to binary
109 | string(String) when is_list(String) ->
110 | Bin = list_to_binary(String),
111 | <>;
112 | string(Bin) when is_binary(Bin) ->
113 | <>.
114 |
115 | %%% Two zero terminated strings.
116 | make_pair(Key, Value) when is_atom(Key) ->
117 | make_pair(atom_to_list(Key), Value);
118 | make_pair(Key, Value) when is_atom(Value) ->
119 | make_pair(Key, atom_to_list(Value));
120 | make_pair(Key, Value) when is_list(Key), is_list(Value) ->
121 | BinKey = list_to_binary(Key),
122 | BinValue = list_to_binary(Value),
123 | make_pair(BinKey, BinValue);
124 | make_pair(Key, Value) when is_binary(Key), is_binary(Value) ->
125 | <>.
127 |
128 | split_pair(Bin, AsBin) when is_binary(Bin) ->
129 | split_pair(binary_to_list(Bin), AsBin);
130 | split_pair(Str, AsBin) ->
131 | split_pair_rec(Str, norec, AsBin).
132 |
133 | split_pair_rec(Bin, AsBin) when is_binary(Bin) ->
134 | split_pair_rec(binary_to_list(Bin), AsBin);
135 | split_pair_rec(Arg, AsBin) ->
136 | split_pair_rec(Arg,[], AsBin).
137 |
138 | split_pair_rec([], Acc, _AsBin) ->
139 | lists:reverse(Acc);
140 | split_pair_rec([0], Acc, _AsBin) ->
141 | lists:reverse(Acc);
142 | split_pair_rec(S, Acc, AsBin) ->
143 | Fun = fun(C) -> C /= 0 end,
144 | {K, [0|S1]} = lists:splitwith(Fun, S),
145 | {V, [0|Tail]} = lists:splitwith(Fun, S1),
146 | {Key, Value} = if AsBin ->
147 | {list_to_binary(K), list_to_binary(V)};
148 | true ->
149 | {K, V}
150 | end,
151 | case Acc of
152 | norec -> {Key, Value};
153 | _ ->
154 | split_pair_rec(Tail, [{Key, Value}| Acc], AsBin)
155 | end.
156 |
157 |
158 | count_string(Bin) when is_binary(Bin) ->
159 | count_string(Bin, 0).
160 |
161 | count_string(<<>>, N) ->
162 | {N, <<>>};
163 | count_string(<<0/integer, Rest/binary>>, N) ->
164 | {N, Rest};
165 | count_string(<<_C/integer, Rest/binary>>, N) ->
166 | count_string(Rest, N+1).
167 |
168 | to_string(Bin, AsBin) when is_binary(Bin) ->
169 | {Count, _} = count_string(Bin, 0),
170 | <> = Bin,
171 | if AsBin ->
172 | {String, Count};
173 | true ->
174 | {binary_to_list(String), Count}
175 | end.
176 |
177 | oids(<<>>, Oids) ->
178 | lists:reverse(Oids);
179 | oids(<>, Oids) ->
180 | oids(Rest, [Oid|Oids]).
181 |
182 | coldescs(<<>>, Descs, _AsBin) ->
183 | lists:reverse(Descs);
184 | coldescs(Bin, Descs, AsBin) ->
185 | {Name, Count} = to_string(Bin, AsBin),
186 | <<_:Count/binary, 0/integer,
187 | TableOID:32/integer,
188 | ColumnNumber:16/integer,
189 | TypeId:32/integer,
190 | TypeSize:16/integer-signed,
191 | TypeMod:32/integer-signed,
192 | FormatCode:16/integer,
193 | Rest/binary>> = Bin,
194 | Format = case FormatCode of
195 | 0 -> text;
196 | 1 -> binary
197 | end,
198 | Desc = {Name, Format, ColumnNumber,
199 | TypeId, TypeSize, TypeMod,
200 | TableOID},
201 | coldescs(Rest, [Desc|Descs], AsBin).
202 |
203 | datacoldescs(N, <<16#ffffffff:32, Rest/binary>>, Descs) when N >= 0 ->
204 | datacoldescs(N-1, Rest, [null|Descs]);
205 | datacoldescs(N,
206 | <>,
207 | Descs) when N >= 0 ->
208 | datacoldescs(N-1, Rest, [Data|Descs]);
209 | datacoldescs(_N, _, Descs) ->
210 | lists:reverse(Descs).
211 |
212 | decode_descs(OidMap, Cols) ->
213 | decode_descs(OidMap, Cols, []).
214 | decode_descs(_OidMap, [], Descs) ->
215 | {ok, lists:reverse(Descs)};
216 | decode_descs(OidMap, [Col|ColTail], Descs) ->
217 | {Name, Format, ColNumber, Oid, _, _, _} = Col,
218 | OidName = dict:fetch(Oid, OidMap),
219 | decode_descs(OidMap, ColTail, [{Name, Format, ColNumber, OidName, [], [], []}|Descs]).
220 |
221 | decode_row(Types, Values, AsBin) ->
222 | decode_row(Types, Values, [], AsBin).
223 | decode_row([], [], Out, _AsBin) ->
224 | {ok, lists:reverse(Out)};
225 | decode_row([Type|TypeTail], [Value|ValueTail], Out0, AsBin) ->
226 | Out1 = decode_col(Type, Value, AsBin),
227 | decode_row(TypeTail, ValueTail, [Out1|Out0], AsBin).
228 |
229 | %decode_col({_, text, _, _, _, _, _}, Value, AsBin) ->
230 | % if AsBin -> Value;
231 | % true -> binary_to_list(Value)
232 | % end;
233 | %decode_col({_Name, _Format, _ColNumber, varchar, _Size, _Modifier, _TableOID}, Value, AsBin) ->
234 | % if AsBin -> Value;
235 | % true -> binary_to_list(Value)
236 | % end;
237 | decode_col({_Name, _Format, _ColNumber, bool, _Size, _Modifier, _TableOID}, Value, _AsBin) ->
238 | B = case Value of
239 | <<0>> -> <<"0">>;
240 | <<1>> -> <<"1">>
241 | end,
242 | {bool, B};
243 | decode_col({_Name, _Format, _ColNumber, int2, _Size, _Modifier, _TableOID}, Value, _AsBin) ->
244 | <> = Value,
245 | {int2, integer_to_binary(Int)};
246 | decode_col({_Name, _Format, _ColNumber, int4, _Size, _Modifier, _TableOID}, Value, _AsBin) ->
247 | <> = Value,
248 | {int4, integer_to_binary(Int)};
249 | decode_col({_Name, _Format, _ColNumber, int8, _Size, _Modifier, _TableOID}, Value, _AsBin) ->
250 | <> = Value,
251 | {int8, integer_to_binary(Int)};
252 | decode_col({_Name, _Format, _ColNumber, numeric, _Size, _Modifier, _TableOID}, Value, _AsBin) ->
253 | N = decode_numeric(Value),
254 | {numeric, N};
255 | decode_col({_Name, _Format, _ColNumber, Oid, _Size, _Modifier, _TableOID}, Value, _AsBin) ->
256 | {Oid, Value}.
257 |
258 | errordesc(Bin, AsBin) ->
259 | errordesc(Bin, [], AsBin).
260 |
261 | errordesc(<<0/integer, _Rest/binary>>, Lines, _AsBin) ->
262 | lists:reverse(Lines);
263 | errordesc(<>, Lines, AsBin) ->
264 | {String, Count} = to_string(Rest, AsBin),
265 | <<_:Count/binary, 0, Rest1/binary>> = Rest,
266 | Msg = case Code of
267 | $S ->
268 | {severity, to_atom(String)};
269 | $C ->
270 | {code, String};
271 | $M ->
272 | {message, String};
273 | $D ->
274 | {detail, String};
275 | $H ->
276 | {hint, String};
277 | $P ->
278 | {position, to_integer(String)};
279 | $p ->
280 | {internal_position, to_integer(String)};
281 | $W ->
282 | {where, String};
283 | $F ->
284 | {file, String};
285 | $L ->
286 | {line, to_integer(String)};
287 | $R ->
288 | {routine, String};
289 | Unknown ->
290 | {Unknown, String}
291 | end,
292 | errordesc(Rest1, [Msg|Lines], AsBin).
293 |
294 | %%% Zip two lists together
295 | zip(List1, List2) ->
296 | zip(List1, List2, []).
297 | zip(List1, List2, Result) when List1 =:= [];
298 | List2 =:= [] ->
299 | lists:reverse(Result);
300 | zip([H1|List1], [H2|List2], Result) ->
301 | zip(List1, List2, [{H1, H2}|Result]).
302 |
303 | %%% Authentication utils
304 |
305 | pass_plain(Password) ->
306 | Pass = [Password, 0],
307 | list_to_binary(Pass).
308 |
309 | %% MD5 authentication patch from
310 | %% Juhani Rankimies
311 | %% (patch slightly rewritten, new bugs are mine :] /Christian Sunesson)
312 |
313 | %%
314 | %% MD5(MD5(password + user) + salt)
315 | %%
316 |
317 | pass_md5(User, Password, Salt) ->
318 | Digest = hex(md5([Password, User])),
319 | Encrypt = hex(md5([Digest, Salt])),
320 | Pass = ["md5", Encrypt, 0],
321 | list_to_binary(Pass).
322 |
323 | to_integer(B) when is_binary(B) ->
324 | to_integer(binary_to_list(B));
325 | to_integer(S) ->
326 | list_to_integer(S).
327 |
328 | to_atom(B) when is_binary(B) ->
329 | to_atom(binary_to_list(B));
330 | to_atom(S) ->
331 | list_to_atom(S).
332 |
333 | hex(B) when is_binary(B) ->
334 | hexlist(binary_to_list(B), []).
335 |
336 | hexlist([], Acc) ->
337 | lists:reverse(Acc);
338 | hexlist([N|Rest], Acc) ->
339 | HighNibble = (N band 16#f0) bsr 4,
340 | LowNibble = (N band 16#0f),
341 | hexlist(Rest, [hexdigit(LowNibble), hexdigit(HighNibble)|Acc]).
342 |
343 | hexdigit(0) -> $0;
344 | hexdigit(1) -> $1;
345 | hexdigit(2) -> $2;
346 | hexdigit(3) -> $3;
347 | hexdigit(4) -> $4;
348 | hexdigit(5) -> $5;
349 | hexdigit(6) -> $6;
350 | hexdigit(7) -> $7;
351 | hexdigit(8) -> $8;
352 | hexdigit(9) -> $9;
353 | hexdigit(10) -> $a;
354 | hexdigit(11) -> $b;
355 | hexdigit(12) -> $c;
356 | hexdigit(13) -> $d;
357 | hexdigit(14) -> $e;
358 | hexdigit(15) -> $f.
359 |
360 | -define(NBASE, 10000).
361 | -define(NUMERIC_POS, 16#0000).
362 | -define(NUMERIC_NEG, 16#4000).
363 | -define(NUMERIC_NAN, 16#C000).
364 |
365 | decode_numeric(<<_Length:16/unsigned, Weight:16/signed,
366 | Sign:16/unsigned, _DScale:16/unsigned, Data/binary>>) ->
367 | case Sign of
368 | ?NUMERIC_NAN ->
369 | <<"NaN">>;
370 | _ ->
371 | N = decode_numeric_digits(Data, Weight, 0),
372 | SN =
373 | case Sign of
374 | ?NUMERIC_POS -> N;
375 | ?NUMERIC_NEG -> -N
376 | end,
377 | if
378 | is_integer(SN) ->
379 | integer_to_binary(SN);
380 | is_float(SN) ->
381 | float_to_binary(SN)
382 | end
383 | end.
384 |
385 | decode_numeric_digits(<<>>, Weight, Res) when Weight < 0 ->
386 | Res;
387 | decode_numeric_digits(<<>>, Weight, Res) ->
388 | decode_numeric_digits(<<>>, Weight - 1, Res * ?NBASE);
389 | decode_numeric_digits(Data, Weight, Res) when Weight < 0 ->
390 | decode_numeric_digits1(Data, Weight, Res);
391 | decode_numeric_digits(<>, Weight, Res) ->
392 | decode_numeric_digits(Data, Weight - 1, Res * ?NBASE + D).
393 |
394 | decode_numeric_digits1(<<>>, _Weight, Res) ->
395 | Res;
396 | decode_numeric_digits1(<>, Weight, Res) ->
397 | decode_numeric_digits1(Data, Weight - 1,
398 | Res + D * math:pow(?NBASE, Weight)).
399 |
400 | %%--------------------------------------------------------------------
401 | %% Connecting stuff
402 | %%--------------------------------------------------------------------
403 | connect(Host, Port, Timeout) ->
404 | case lookup(Host, Timeout) of
405 | {ok, AddrsFamilies} ->
406 | do_connect(AddrsFamilies, Port, Timeout, {error, nxdomain});
407 | {error, _} = Err ->
408 | Err
409 | end.
410 |
411 | do_connect([{IP, Family}|AddrsFamilies], Port1, Timeout, _Err) ->
412 | Port = case Family of
413 | local -> 0;
414 | _ -> Port1
415 | end,
416 | case gen_tcp:connect(IP, Port, [{active, false}, binary,
417 | {packet, raw}, Family], Timeout) of
418 | {ok, Sock} ->
419 | {ok, Sock};
420 | {error, _} = Err ->
421 | do_connect(AddrsFamilies, Port, Timeout, Err)
422 | end;
423 | do_connect([], _Port, _Timeout, Err) ->
424 | Err.
425 |
426 | lookup([$u,$n,$i,$x,$: | Path], _Timeout) ->
427 | {ok, [{{local, Path}, local}]};
428 | lookup(Host, Timeout) ->
429 | case inet:parse_address(Host) of
430 | {ok, IP} ->
431 | {ok, [{IP, get_addr_type(IP)}]};
432 | {error, _} ->
433 | do_lookup([{Host, Family} || Family <- [inet6, inet]],
434 | Timeout, [], {error, nxdomain})
435 | end.
436 |
437 | do_lookup([{Host, Family}|HostFamilies], Timeout, AddrFamilies, Err) ->
438 | case inet:gethostbyname(Host, Family, Timeout) of
439 | {ok, HostEntry} ->
440 | Addrs = host_entry_to_addrs(HostEntry),
441 | AddrFamilies1 = [{Addr, Family} || Addr <- Addrs],
442 | do_lookup(HostFamilies, Timeout,
443 | AddrFamilies ++ AddrFamilies1,
444 | Err);
445 | {error, _} = Err1 ->
446 | do_lookup(HostFamilies, Timeout, AddrFamilies, Err1)
447 | end;
448 | do_lookup([], _Timeout, [], Err) ->
449 | Err;
450 | do_lookup([], _Timeout, AddrFamilies, _Err) ->
451 | {ok, AddrFamilies}.
452 |
453 | host_entry_to_addrs(#hostent{h_addr_list = AddrList}) ->
454 | lists:filter(
455 | fun(Addr) ->
456 | try get_addr_type(Addr) of
457 | _ -> true
458 | catch _:badarg ->
459 | false
460 | end
461 | end, AddrList).
462 |
463 | get_addr_type({_, _, _, _}) -> inet;
464 | get_addr_type({_, _, _, _, _, _, _, _}) -> inet6;
465 | get_addr_type(_) -> erlang:error(badarg).
466 |
--------------------------------------------------------------------------------