├── .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 | [![CI](https://github.com/processone/p1_pgsql/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/processone/p1_pgsql/actions/workflows/ci.yml) 4 | [![Coverage Status](https://coveralls.io/repos/processone/p1_pgsql/badge.svg?branch=master&service=github)](https://coveralls.io/github/processone/p1_pgsql?branch=master) 5 | [![Hex version](https://img.shields.io/hexpm/v/p1_pgsql.svg "Hex version")](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 = < 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 | --------------------------------------------------------------------------------