├── VERSION ├── .github ├── FUNDING.yml ├── linters │ └── .markdown-lint.yml └── workflows │ ├── lint-action-workflows.yml │ ├── release-notes-reminder.yml │ ├── remove-discuss-during-sync.yml │ ├── add-discuss-during-sync.yml │ ├── generate-documentation.yml │ ├── changelog-bot.yml │ ├── release-notes.yml │ ├── breakage-against-ponyc-latest.yml │ ├── pr.yml │ ├── announce-a-release.yml │ ├── release.yml │ └── prepare-for-a-release.yml ├── .gitignore ├── .markdownlintignore ├── postgres ├── simple_query.pony ├── row.pony ├── field_data_types.pony ├── field.pony ├── _authentication_failure_reason.pony ├── _authentication_request_type.pony ├── _md5_password.pony ├── result_receiver.pony ├── _message_type.pony ├── session_status_notify.pony ├── query_error.pony ├── result.pony ├── _mort.pony ├── _response_message_parser.pony ├── _test_frontend_message.pony ├── rows.pony ├── _backend_messages.pony ├── _frontend_message.pony ├── error_response_message.pony ├── _response_parser.pony ├── _test.pony ├── _test_query.pony ├── session.pony └── _test_response_parser.pony ├── STYLE_GUIDE.md ├── .release-notes ├── next-release.md ├── 0.1.0.md ├── 0.2.1.md ├── 0.1.1.md ├── 0.2.2.md └── 0.2.0.md ├── corral.json ├── CHANGELOG.md ├── RELEASE_PROCESS.md ├── LICENSE ├── examples └── query │ └── query-example.pony ├── README.md ├── Makefile ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.2 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: ponyc 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _repos/ 2 | _corral/ 3 | build/ 4 | lock.json 5 | -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false 3 | } 4 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | CODE_OF_CONDUCT.md 3 | .release-notes/ 4 | -------------------------------------------------------------------------------- /postgres/simple_query.pony: -------------------------------------------------------------------------------- 1 | class val SimpleQuery 2 | let string: String 3 | 4 | new val create(string': String) => 5 | string = string' 6 | -------------------------------------------------------------------------------- /postgres/row.pony: -------------------------------------------------------------------------------- 1 | class val Row 2 | let fields: Array[Field] val 3 | 4 | new val create(fields': Array[Field] val) => 5 | fields = fields' 6 | -------------------------------------------------------------------------------- /STYLE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Style Guide 2 | 3 | postgres follows the [Pony standard library Style Guide](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md). 4 | -------------------------------------------------------------------------------- /.release-notes/next-release.md: -------------------------------------------------------------------------------- 1 | ## Update ponylang/ssl dependency to 1.0.1 2 | 3 | We've updated the ponylang/ssl library dependency in this project to 1.0.1. 4 | 5 | -------------------------------------------------------------------------------- /postgres/field_data_types.pony: -------------------------------------------------------------------------------- 1 | type FieldDataTypes is 2 | ( Bool 3 | | F32 4 | | F64 5 | | I16 6 | | I32 7 | | I64 8 | | None 9 | | String ) 10 | -------------------------------------------------------------------------------- /postgres/field.pony: -------------------------------------------------------------------------------- 1 | class val Field 2 | let name: String 3 | let value: FieldDataTypes 4 | 5 | new val create(name': String, value': FieldDataTypes) => 6 | name = name' 7 | value = value' 8 | -------------------------------------------------------------------------------- /.release-notes/0.1.0.md: -------------------------------------------------------------------------------- 1 | ## Initial version 2 | 3 | Initial version of ponylang/postgres. See the [README](https://github.com/ponylang/postgres/blob/main/README.md) for the current status of this alpha-level project. 4 | -------------------------------------------------------------------------------- /postgres/_authentication_failure_reason.pony: -------------------------------------------------------------------------------- 1 | type AuthenticationFailureReason is 2 | ( InvalidAuthenticationSpecification 3 | | InvalidPassword ) 4 | 5 | primitive InvalidAuthenticationSpecification 6 | primitive InvalidPassword 7 | -------------------------------------------------------------------------------- /.release-notes/0.2.1.md: -------------------------------------------------------------------------------- 1 | ## Update ponylang/lori dependency to 0.6.1 2 | 3 | We've updated the dependency on [ponylang/lori](https://github.com/ponylang/lori) to version 0.6.1. The new version of lori comes with some stability improvements. 4 | -------------------------------------------------------------------------------- /.release-notes/0.1.1.md: -------------------------------------------------------------------------------- 1 | ## Update ponylang/lori dependency to 0.5.1 2 | 3 | We've updated the dependency on `ponylang/lori` to version 0.5.1. This new version of Lori addresses a couple of race conditions and should generally improve the stability of application using this library. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.2.2.md: -------------------------------------------------------------------------------- 1 | ## Change SSL dependency 2 | 3 | We've switched our SSL dependency from `ponylang/crypto` to `ponylang/ssl`. `ponylang/crypto` is deprecated and will soon receive no further updates. 4 | 5 | As part of the change, we also had to update the `ponylang/lori` dependency to version 0.6.2. 6 | -------------------------------------------------------------------------------- /postgres/_authentication_request_type.pony: -------------------------------------------------------------------------------- 1 | primitive _AuthenticationRequestType 2 | """ 3 | Authentication request types 4 | 5 | See: https://www.postgresql.org/docs/current/protocol-message-formats.html 6 | """ 7 | fun ok(): I32 => 8 | 0 9 | 10 | fun md5_password(): I32 => 11 | 5 12 | -------------------------------------------------------------------------------- /postgres/_md5_password.pony: -------------------------------------------------------------------------------- 1 | use "ssl/crypto" 2 | 3 | primitive _MD5Password 4 | """ 5 | Constructs a validly formatted Postgres MD5 password. 6 | """ 7 | fun apply(username: String, password: String, salt: String): String => 8 | "md5" + ToHexString(MD5(ToHexString(MD5(password + username)) + salt)) 9 | -------------------------------------------------------------------------------- /postgres/result_receiver.pony: -------------------------------------------------------------------------------- 1 | // TODO SEAN: consider if each of these should take the session as well. If yes, 2 | // it means that on success or failure, an actor that knows nothing about the 3 | // session (ie no tag) could use it to execute additional commands after getting 4 | // results. There are pros and cons to that. 5 | interface tag ResultReceiver 6 | be pg_query_result(result: Result) 7 | """ 8 | """ 9 | 10 | be pg_query_failed(query: SimpleQuery, failure: (ErrorResponseMessage | ClientQueryError)) 11 | """ 12 | """ 13 | -------------------------------------------------------------------------------- /.github/workflows/lint-action-workflows.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Action Workflows 2 | 3 | on: pull_request 4 | 5 | concurrency: 6 | group: lint-actions-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | permissions: 10 | packages: read 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4.1.1 19 | - name: Check workflow files 20 | uses: docker://ghcr.io/ponylang/shared-docker-ci-actionlint:20250119 21 | with: 22 | args: -color 23 | -------------------------------------------------------------------------------- /.github/workflows/release-notes-reminder.yml: -------------------------------------------------------------------------------- 1 | name: Release Notes Reminder 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - labeled 7 | 8 | permissions: 9 | packages: read 10 | pull-requests: write 11 | 12 | jobs: 13 | release-note-reminder: 14 | runs-on: ubuntu-latest 15 | name: Prompt to add release notes 16 | steps: 17 | - name: Prompt to add release notes 18 | uses: docker://ghcr.io/ponylang/release-notes-reminder-bot-action:0.1.1 19 | env: 20 | API_CREDENTIALS: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/remove-discuss-during-sync.yml: -------------------------------------------------------------------------------- 1 | name: Remove discuss during sync label 2 | 3 | on: 4 | issues: 5 | types: 6 | - closed 7 | pull_request_target: 8 | types: 9 | - closed 10 | 11 | permissions: 12 | pull-requests: write 13 | 14 | jobs: 15 | remove-label: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Remove label 19 | uses: andymckay/labeler@467347716a3bdbca7f277cb6cd5fa9c5205c5412 20 | with: 21 | repo-token: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 22 | remove-labels: "discuss during sync" 23 | -------------------------------------------------------------------------------- /corral.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "postgres" 4 | ], 5 | "deps": [ 6 | { 7 | "locator": "github.com/ponylang/ssl.git", 8 | "version": "1.0.1" 9 | }, 10 | { 11 | "locator": "github.com/ponylang/lori.git", 12 | "version": "0.6.2" 13 | } 14 | ], 15 | "info": { 16 | "description": "A pure Pony Postgres driver", 17 | "homepage": "https://github.com/ponylang/postgres/", 18 | "license": "BSD-2-Clause", 19 | "documentation_url": "https://ponylang.github.io/postgres", 20 | "version": "0.2.2", 21 | "name": "postgres" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /postgres/_message_type.pony: -------------------------------------------------------------------------------- 1 | primitive _MessageType 2 | """ 3 | Code that is the first byte of each message. 4 | 5 | See: https://www.postgresql.org/docs/current/protocol-message-formats.html 6 | """ 7 | fun authentication_request(): U8 => 8 | 'R' 9 | 10 | fun command_complete(): U8 => 11 | 'C' 12 | 13 | fun data_row(): U8 => 14 | 'D' 15 | 16 | fun empty_query_response(): U8 => 17 | 'I' 18 | 19 | fun error_response(): U8 => 20 | 'E' 21 | 22 | fun query(): U8 => 23 | 'Q' 24 | 25 | fun ready_for_query(): U8 => 26 | 'Z' 27 | 28 | fun row_description(): U8 => 29 | 'T' 30 | -------------------------------------------------------------------------------- /.release-notes/0.2.0.md: -------------------------------------------------------------------------------- 1 | ## Update ponylang/lori dependency to 0.6.0 2 | 3 | We've updated the dependency on [ponylang/lori](https://github.com/ponylang/lori) to version 0.6.0. The new version of lori comes with a number of stability improvements that should make this library more reliable in "edge-case scenarios". 4 | 5 | The new lori dependency has a transitive dependency on [ponylang/net_ssl](https://github.com/ponylang/net_ssl) and as such, an SSL library is now required to build this library. Please see the [net_ssl installation instructions](https://github.com/ponylang/net_ssl?tab=readme-ov-file#installation) for more information. 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/add-discuss-during-sync.yml: -------------------------------------------------------------------------------- 1 | name: Add discuss during sync label 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | issue_comment: 9 | types: 10 | - created 11 | pull_request_target: 12 | types: 13 | - opened 14 | - edited 15 | - ready_for_review 16 | - reopened 17 | pull_request_review: 18 | types: 19 | - submitted 20 | 21 | permissions: 22 | pull-requests: write 23 | 24 | jobs: 25 | add-label: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Add "discuss during sync" label to active GH entity 29 | uses: andymckay/labeler@467347716a3bdbca7f277cb6cd5fa9c5205c5412 30 | with: 31 | repo-token: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 32 | add-labels: "discuss during sync" 33 | -------------------------------------------------------------------------------- /postgres/session_status_notify.pony: -------------------------------------------------------------------------------- 1 | interface tag SessionStatusNotify 2 | be pg_session_connected(session: Session) => 3 | """ 4 | Called when we have connected to the server but haven't yet tried to 5 | authenticate. 6 | """ 7 | None 8 | 9 | be pg_session_connection_failed(session: Session) => 10 | """ 11 | Called when we have failed to connect to the server before attempting to 12 | authenticate. 13 | """ 14 | None 15 | 16 | be pg_session_authenticated(session: Session) => 17 | """ 18 | Called when we have successfully authenticated with the server. 19 | """ 20 | None 21 | 22 | be pg_session_authentication_failed( 23 | session: Session, 24 | reason: AuthenticationFailureReason) 25 | => 26 | """ 27 | Called if we have failed to successfully authenicate with the server. 28 | """ 29 | None 30 | 31 | be pg_session_shutdown(session: Session) => 32 | """ 33 | Called when a session ends. 34 | """ 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a CHANGELOG](http://keepachangelog.com/). 4 | 5 | ## [unreleased] - unreleased 6 | 7 | ### Fixed 8 | 9 | 10 | ### Added 11 | 12 | 13 | ### Changed 14 | 15 | - Update ponylang/ssl dependency ([PR #55](https://github.com/ponylang/postgres/pull/55)) 16 | 17 | ## [0.2.2] - 2025-07-16 18 | 19 | ### Changed 20 | 21 | - Changed SSL dependency ([PR #54](https://github.com/ponylang/postgres/pull/54)) 22 | 23 | ## [0.2.1] - 2025-03-04 24 | 25 | ### Changed 26 | 27 | - Update ponylang/lori dependency to 0.6.1 ([PR #52](https://github.com/ponylang/postgres/pull/52) 28 | 29 | ## [0.2.0] - 2025-03-02 30 | 31 | ### Changed 32 | 33 | - Update ponylang/lori dependency to 0.6.0 ([PR #51](https://github.com/ponylang/postgres/pull/51)) 34 | 35 | ## [0.1.1] - 2025-02-13 36 | 37 | ### Changed 38 | 39 | - Update ponylang/lori dependency to 0.5.1 ([PR #50](https://github.com/ponylang/postgres/pull/50)) 40 | 41 | ## [0.1.0] - 2023-02-12 42 | 43 | ### Added 44 | 45 | - Initial version 46 | 47 | -------------------------------------------------------------------------------- /postgres/query_error.pony: -------------------------------------------------------------------------------- 1 | trait val ClientQueryError 2 | 3 | primitive SesssionNeverOpened is ClientQueryError 4 | """ 5 | Error returned when a query is attempted for a session that hasn't been opened 6 | yet or is in the process of being opened. 7 | """ 8 | 9 | primitive SessionClosed is ClientQueryError 10 | """ 11 | Error returned when a query is attempted for a session that was closed or 12 | failed to open. Includes sessions that were closed by the user as well as 13 | those closed due to connection failures, authentication failures, and 14 | connections that have been shut down due to unrecoverable Postgres protocol 15 | errors. 16 | """ 17 | 18 | primitive SessionNotAuthenticated is ClientQueryError 19 | """ 20 | Error returned when a query is attempted for a session that is open but hasn't 21 | been authenticated yet. 22 | """ 23 | 24 | primitive DataError is ClientQueryError 25 | """ 26 | Error returned when the data that came back from a query is in a format that 27 | this library doesn't expect. This might indicate something like, the number 28 | of columns across rows returned doesn't match or other "this should never 29 | happen" type of errors. 30 | """ 31 | -------------------------------------------------------------------------------- /postgres/result.pony: -------------------------------------------------------------------------------- 1 | trait val Result 2 | fun query(): SimpleQuery 3 | 4 | class val ResultSet is Result 5 | let _query: SimpleQuery 6 | let _rows: Rows 7 | let _command: String 8 | 9 | new val create(query': SimpleQuery, 10 | rows': Rows, 11 | command': String) 12 | => 13 | _query = query' 14 | _rows = rows' 15 | _command = command' 16 | 17 | fun query(): SimpleQuery => 18 | _query 19 | 20 | fun rows(): Rows => 21 | _rows 22 | 23 | fun command(): String => 24 | _command 25 | 26 | class val SimpleResult is Result 27 | let _query: SimpleQuery 28 | 29 | new val create(query': SimpleQuery) => 30 | _query = query' 31 | 32 | fun query(): SimpleQuery => 33 | _query 34 | 35 | class val RowModifying is Result 36 | let _query: SimpleQuery 37 | let _command: String 38 | let _impacted: USize 39 | 40 | new val create(query': SimpleQuery, 41 | command': String, 42 | impacted': USize) 43 | => 44 | _query = query' 45 | _command = command' 46 | _impacted = impacted' 47 | 48 | fun query(): SimpleQuery => 49 | _query 50 | 51 | fun command(): String => 52 | _command 53 | 54 | fun impacted(): USize => 55 | _impacted 56 | -------------------------------------------------------------------------------- /postgres/_mort.pony: -------------------------------------------------------------------------------- 1 | use @exit[None](status: U8) 2 | use @fprintf[I32](stream: Pointer[U8] tag, fmt: Pointer[U8] tag, ...) 3 | use @pony_os_stderr[Pointer[U8]]() 4 | 5 | primitive _IllegalState 6 | """ 7 | To be used to exit early if we called a function that shouldn't be possible 8 | in our current state. 9 | """ 10 | fun apply(loc: SourceLoc = __loc) => 11 | @fprintf( 12 | @pony_os_stderr(), 13 | ("An illegal state was encountered in %s at line %s\n" + 14 | "Please open an issue at https://github.com/ponylang/postgres/issues") 15 | .cstring(), 16 | loc.file().cstring(), 17 | loc.line().string().cstring()) 18 | @exit(1) 19 | 20 | primitive _Unreachable 21 | """ 22 | To be used in places that the compiler can't prove is unreachable but we are 23 | certain is unreachable and if we reach it, we'd be silently hiding a bug. 24 | """ 25 | fun apply(loc: SourceLoc = __loc) => 26 | @fprintf( 27 | @pony_os_stderr(), 28 | ("The unreachable was reached in %s at line %s\n" + 29 | "Please open an issue at https://github.com/ponylang/postgres/issues") 30 | .cstring(), 31 | loc.file().cstring(), 32 | loc.line().string().cstring()) 33 | @exit(1) 34 | -------------------------------------------------------------------------------- /RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # How to cut a release 2 | 3 | This document is aimed at members of the Pony team who might be cutting a release of this library. It serves as a checklist that can take you through doing a release step-by-step. 4 | 5 | ## Prerequisites 6 | 7 | You must have commit access to this repository 8 | 9 | ## Releasing 10 | 11 | Please note that this document was written with the assumption that you are using a clone of the `postgres` repo. You have to be using a clone rather than a fork. It is advised to your do this by making a fresh clone of the `postgres` repo from which you will release. 12 | 13 | ```bash 14 | git clone git@github.com:ponylang/postgres.git postgres-release-clean 15 | cd postgres-release-clean 16 | ``` 17 | 18 | Before getting started, you will need a number for the version that you will be releasing as well as an agreed upon "golden commit" that will form the basis of the release. 19 | 20 | The "golden commit" must be `HEAD` on the `main` branch of this repository. At this time, releasing from any other location is not supported. 21 | 22 | For the duration of this document, that we are releasing version is `0.3.1`. Any place you see those values, please substitute your own version. 23 | 24 | ```bash 25 | git tag release-0.3.1 26 | git push origin release-0.3.1 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/workflows/generate-documentation.yml: -------------------------------------------------------------------------------- 1 | name: Manually generate documentation 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | packages: read 11 | 12 | concurrency: 13 | group: "update-documentation" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | generate-documentation: 18 | name: Generate documentation for release 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | container: 24 | image: ghcr.io/ponylang/library-documentation-action-v2-insiders:release 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4.1.1 28 | - name: Generate documentation 29 | run: /entrypoint.py 30 | env: 31 | INPUT_SITE_URL: "https://ponylang.github.io/postgres/" 32 | INPUT_LIBRARY_NAME: "postgres" 33 | INPUT_DOCS_BUILD_DIR: "build/postgres-docs" 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: 'build/postgres-docs/site/' 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v4 43 | -------------------------------------------------------------------------------- /.github/workflows/changelog-bot.yml: -------------------------------------------------------------------------------- 1 | name: Changelog Bot 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - CHANGELOG.md 11 | 12 | permissions: 13 | packages: read 14 | pull-requests: read 15 | contents: write 16 | 17 | jobs: 18 | changelog-bot: 19 | runs-on: ubuntu-latest 20 | name: Update CHANGELOG.md 21 | steps: 22 | - name: Update Changelog 23 | uses: docker://ghcr.io/ponylang/changelog-bot-action:0.3.7 24 | with: 25 | GIT_USER_NAME: "Ponylang Main Bot" 26 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Send alert on failure 30 | if: ${{ failure() }} 31 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 32 | with: 33 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 34 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 35 | organization-url: 'https://ponylang.zulipchat.com/' 36 | to: notifications 37 | type: stream 38 | topic: ${{ github.repository }} unattended job failure 39 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 40 | -------------------------------------------------------------------------------- /.github/workflows/release-notes.yml: -------------------------------------------------------------------------------- 1 | name: Release Notes 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - .release-notes/next-release.md 11 | - .release-notes/\d+.\d+.\d+.md 12 | 13 | permissions: 14 | packages: read 15 | pull-requests: read 16 | contents: write 17 | 18 | jobs: 19 | release-notes: 20 | runs-on: ubuntu-latest 21 | name: Update release notes 22 | steps: 23 | - name: Update 24 | uses: docker://ghcr.io/ponylang/release-notes-bot-action:0.3.10 25 | with: 26 | GIT_USER_NAME: "Ponylang Main Bot" 27 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 28 | env: 29 | API_CREDENTIALS: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Send alert on failure 31 | if: ${{ failure() }} 32 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 33 | with: 34 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 35 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 36 | organization-url: 'https://ponylang.zulipchat.com/' 37 | to: notifications 38 | type: stream 39 | topic: ${{ github.repository }} scheduled job failure 40 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021, The Pony Developers 2 | Copyright (C) 2021, Andreas Stührk 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /postgres/_response_message_parser.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | 3 | primitive _ResponseMessageParser 4 | fun apply(s: Session ref, readbuf: Reader) => 5 | try 6 | match _ResponseParser(readbuf)? 7 | | let msg: _AuthenticationMD5PasswordMessage => 8 | s.state.on_authentication_md5_password(s, msg) 9 | | _AuthenticationOkMessage => 10 | s.state.on_authentication_ok(s) 11 | | let msg: _CommandCompleteMessage => 12 | s.state.on_command_complete(s, msg) 13 | | let msg: _DataRowMessage => 14 | s.state.on_data_row(s, msg) 15 | | let err: ErrorResponseMessage => 16 | match err.code 17 | | "28000" => 18 | s.state.on_authentication_failed(s, 19 | InvalidAuthenticationSpecification) 20 | return 21 | | "28P01" => 22 | s.state.on_authentication_failed(s, InvalidPassword) 23 | return 24 | else 25 | s.state.on_error_response(s, err) 26 | end 27 | | let msg: _ReadyForQueryMessage => 28 | s.state.on_ready_for_query(s, msg) 29 | | let msg: _RowDescriptionMessage => 30 | s.state.on_row_description(s, msg) 31 | | let msg: _EmptyQueryResponseMessage => 32 | s.state.on_empty_query_response(s) 33 | | None => 34 | // No complete message was found. Stop parsing for now. 35 | return 36 | end 37 | else 38 | // An unrecoverable error was encountered while parsing. Once that 39 | // happens, there's no way we are going to be able to figure out how 40 | // to get the responses back into an understandable state. The only 41 | // thing we can do is shut the session down. 42 | 43 | s.state.shutdown(s) 44 | return 45 | end 46 | 47 | s._process_again() 48 | -------------------------------------------------------------------------------- /postgres/_test_frontend_message.pony: -------------------------------------------------------------------------------- 1 | use "pony_test" 2 | 3 | class \nodoc\ iso _TestFrontendMessagePassword is UnitTest 4 | fun name(): String => 5 | "FrontendMessage/Password" 6 | 7 | fun apply(h: TestHelper) => 8 | let password = "pwd" 9 | let expected: Array[U8] = ifdef bigendian then 10 | ['p'; 8; 0; 0; 0; 'p'; 'w'; 'd'; 0] 11 | else 12 | ['p'; 0; 0; 0; 8; 'p'; 'w'; 'd'; 0] 13 | end 14 | 15 | h.assert_array_eq[U8](expected, _FrontendMessage.password(password)) 16 | 17 | class \nodoc\ iso _TestFrontendMessageQuery is UnitTest 18 | fun name(): String => 19 | "FrontendMessage/Query" 20 | 21 | fun apply(h: TestHelper) => 22 | let query = "select * from free_candy" 23 | let expected: Array[U8] = ifdef bigendian then 24 | [ 81; 29; 0; 0; 0; 115; 101; 108; 101; 99; 116; 32; 42; 32; 102; 114 25 | 111; 109; 32; 102; 114; 101; 101; 95; 99; 97; 110; 100; 121; 0 ] 26 | else 27 | [ 81; 0; 0; 0; 29; 115; 101; 108; 101; 99; 116; 32; 42; 32; 102; 114 28 | 111; 109; 32; 102; 114; 101; 101; 95; 99; 97; 110; 100; 121; 0 ] 29 | end 30 | 31 | h.assert_array_eq[U8](expected, _FrontendMessage.query(query)) 32 | 33 | class \nodoc\ iso _TestFrontendMessageStartup is UnitTest 34 | fun name(): String => 35 | "FrontendMessage/Startup" 36 | 37 | fun apply(h: TestHelper) => 38 | let username = "pony" 39 | let password = "7669" 40 | let expected: Array[U8] = ifdef bigendian then 41 | [ 33; 0; 0; 0; 3; 0; 0; 0; 117; 115; 101; 114; 0; 112; 111; 110; 121; 0 42 | 100; 97; 116; 97; 98; 97; 115; 101; 0; 55; 54; 54; 57; 0; 0 ] 43 | else 44 | [ 0; 0; 0; 33; 0; 3; 0; 0; 117; 115; 101; 114; 0; 112; 111; 110; 121; 0 45 | 100; 97; 116; 97; 98; 97; 115; 101; 0; 55; 54; 54; 57; 0; 0 ] 46 | end 47 | 48 | h.assert_array_eq[U8](expected, _FrontendMessage.startup(username, password)) 49 | -------------------------------------------------------------------------------- /.github/workflows/breakage-against-ponyc-latest.yml: -------------------------------------------------------------------------------- 1 | name: Test against ponyc nightly 2 | 3 | on: 4 | repository_dispatch: 5 | types: 6 | - shared-docker-builders-updated 7 | 8 | permissions: 9 | packages: read 10 | 11 | jobs: 12 | vs-latest-ponyc-linux: 13 | name: Verify main against the latest ponyc om Linux 14 | runs-on: ubuntu-latest 15 | container: 16 | image: ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:nightly 17 | services: 18 | postgres: 19 | image: postgres:14.5 20 | env: 21 | POSTGRES_DB: postgres 22 | POSTGRES_USER: postgres 23 | POSTGRES_PASSWORD: postgres 24 | POSTGRES_HOST_AUTH_METHOD: md5 25 | POSTGRES_INITDB_ARGS: "--auth-host=md5" 26 | # Set health checks to wait until postgres has started 27 | options: >- 28 | --health-cmd pg_isready 29 | --health-interval 10s 30 | --health-timeout 5s 31 | --health-retries 5 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v4.1.1 35 | - name: Unit tests 36 | run: make unit-tests config=debug ssl=0.9.0 37 | - name: Integration tests 38 | run: make integration-tests config=debug ssl=0.9.0 39 | env: 40 | POSTGRES_HOST: postgres 41 | POSTGRES_PORT: 5432 42 | POSTGRES_USERNAME: postgres 43 | POSTGRES_PASSWORD: postgres 44 | POSTGRES_DATABASE: postgres 45 | - name: Send alert on failure 46 | if: ${{ failure() }} 47 | uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 48 | with: 49 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 50 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 51 | organization-url: 'https://ponylang.zulipchat.com/' 52 | to: notifications 53 | type: stream 54 | topic: ${{ github.repository }} scheduled job failure 55 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 56 | -------------------------------------------------------------------------------- /examples/query/query-example.pony: -------------------------------------------------------------------------------- 1 | // in your code this `use` statement would be: 2 | // use "postgres" 3 | use "cli" 4 | use "collections" 5 | use lori = "lori" 6 | use "../../postgres" 7 | 8 | actor Main 9 | new create(env: Env) => 10 | let server_info = ServerInfo(env.vars) 11 | let auth = lori.TCPConnectAuth(env.root) 12 | 13 | let client = Client(auth, server_info, env.out) 14 | 15 | actor Client is (SessionStatusNotify & ResultReceiver) 16 | let _session: Session 17 | let _out: OutStream 18 | 19 | new create(auth: lori.TCPConnectAuth, info: ServerInfo, out: OutStream) => 20 | _out = out 21 | _session = Session( 22 | auth, 23 | this, 24 | info.host, 25 | info.port, 26 | info.username, 27 | info.password, 28 | info.database) 29 | 30 | be close() => 31 | _session.close() 32 | 33 | be pg_session_authenticated(session: Session) => 34 | _out.print("Authenticated.") 35 | _out.print("Sending query....") 36 | let q = SimpleQuery("SELECT 525600::text") 37 | session.execute(q, this) 38 | 39 | be pg_session_authentication_failed( 40 | s: Session, 41 | reason: AuthenticationFailureReason) 42 | => 43 | _out.print("Failed to authenticate.") 44 | 45 | be pg_query_result(result: Result) => 46 | _out.print("Query result received.") 47 | // We got a result which for our example program is our trigger to shutdown. 48 | close() 49 | 50 | be pg_query_failed(query: SimpleQuery, 51 | failure: (ErrorResponseMessage | ClientQueryError)) 52 | => 53 | _out.print("Query failed.") 54 | // Our example program is failing, we want to exit so, let's shut down the 55 | // connection. 56 | close() 57 | 58 | class val ServerInfo 59 | let host: String 60 | let port: String 61 | let username: String 62 | let password: String 63 | let database: String 64 | 65 | new val create(vars: (Array[String] val | None)) => 66 | let e = EnvVars(vars) 67 | host = try e("POSTGRES_HOST")? else "127.0.0.1" end 68 | port = try e("POSTGRES_PORT")? else "5432" end 69 | username = try e("POSTGRES_USERNAME")? else "postgres" end 70 | password = try e("POSTGRES_PASSWORD")? else "postgres" end 71 | database = try e("POSTGRES_DATABASE")? else "postgres" end 72 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: pull_request 4 | 5 | concurrency: 6 | group: pr-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | permissions: 10 | packages: read 11 | 12 | jobs: 13 | superlinter: 14 | name: Lint bash, docker, markdown, and yaml 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4.1.1 18 | - name: Lint codebase 19 | uses: docker://github/super-linter:v3.8.3 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | VALIDATE_ALL_CODEBASE: true 23 | VALIDATE_BASH: true 24 | VALIDATE_DOCKERFILE: true 25 | VALIDATE_MD: true 26 | VALIDATE_YAML: true 27 | 28 | verify-changelog: 29 | name: Verify CHANGELOG is valid 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4.1.1 33 | - name: Verify CHANGELOG 34 | uses: docker://ghcr.io/ponylang/changelog-tool:release 35 | with: 36 | args: changelog-tool verify 37 | 38 | vs-release-ponyc-linux: 39 | name: Test against recent ponyc release on Linux 40 | runs-on: ubuntu-latest 41 | container: 42 | image: ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release 43 | services: 44 | postgres: 45 | image: postgres:14.5 46 | env: 47 | POSTGRES_DB: postgres 48 | POSTGRES_USER: postgres 49 | POSTGRES_PASSWORD: postgres 50 | POSTGRES_HOST_AUTH_METHOD: md5 51 | POSTGRES_INITDB_ARGS: "--auth-host=md5" 52 | # Set health checks to wait until postgres has started 53 | options: >- 54 | --health-cmd pg_isready 55 | --health-interval 10s 56 | --health-timeout 5s 57 | --health-retries 5 58 | steps: 59 | - name: Checkout code 60 | uses: actions/checkout@v4.1.1 61 | - name: Unit tests 62 | run: make unit-tests config=debug ssl=0.9.0 63 | - name: Integration tests 64 | run: make integration-tests config=debug ssl=0.9.0 65 | env: 66 | POSTGRES_HOST: postgres 67 | POSTGRES_PORT: 5432 68 | POSTGRES_USERNAME: postgres 69 | POSTGRES_PASSWORD: postgres 70 | POSTGRES_DATABASE: postgres 71 | -------------------------------------------------------------------------------- /postgres/rows.pony: -------------------------------------------------------------------------------- 1 | class val Rows 2 | let _rows: Array[Row] val 3 | 4 | new val create(rows': Array[Row] val) => 5 | _rows = rows' 6 | 7 | fun size(): USize => 8 | """ 9 | Returns the number of rows. 10 | """ 11 | _rows.size() 12 | 13 | fun apply(i: USize): Row ? => 14 | """ 15 | Returns the `i`th row if it exists. Otherwise, throws an error. 16 | """ 17 | _rows(i)? 18 | 19 | fun values(): RowIterator => 20 | """ 21 | Returns an iterator over the rows. 22 | """ 23 | RowIterator._create(_rows) 24 | 25 | class RowIterator is Iterator[Row] 26 | let _array: Array[Row] val 27 | var _i: USize 28 | 29 | new _create(array: Array[Row] val) => 30 | _array = array 31 | _i = 0 32 | 33 | fun has_next(): Bool => 34 | _i < _array.size() 35 | 36 | fun ref next(): Row ? => 37 | _array(_i = _i + 1)? 38 | 39 | fun ref rewind(): RowIterator => 40 | _i = 0 41 | this 42 | 43 | // TODO need tests for all this 44 | // In order to easily test it though, we need to add a decent chunk of operators 45 | // to be able to compare the structure of the objects. Really, we need 'eq' on 46 | // 'Rows', 'Row' and 'Field'. 47 | primitive _RowsBuilder 48 | fun apply(rows': Array[Array[(String|None)] val] val, 49 | row_descriptions': Array[(String, U32)] val): Rows ? 50 | => 51 | let rows = recover iso Array[Row] end 52 | for row in rows'.values() do 53 | let fields = recover iso Array[Field] end 54 | for (i, v) in row.pairs() do 55 | let desc = row_descriptions'(i)? 56 | let field_name = desc._1 57 | let field_type = desc._2 58 | let field_value = _field_to_type(v, field_type)? 59 | let field = Field(field_name, field_value) 60 | fields.push(field) 61 | end 62 | rows.push(Row(consume fields)) 63 | end 64 | Rows(consume rows) 65 | 66 | fun _field_to_type(field: (String | None), type_id: U32): FieldDataTypes ? => 67 | match field 68 | | let f: String => 69 | match type_id 70 | | 16 => f.at("t") 71 | | 20 => f.i64()? 72 | | 21 => f.i16()? 73 | | 23 => f.i32()? 74 | | 700 => f.f32()? 75 | | 701 => f.f64()? 76 | else 77 | f 78 | end 79 | | None => 80 | None 81 | end 82 | -------------------------------------------------------------------------------- /.github/workflows/announce-a-release.yml: -------------------------------------------------------------------------------- 1 | name: Announce a release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'announce-[0-9]+.[0-9]+.[0-9]+' 7 | 8 | concurrency: announce-a-release 9 | 10 | permissions: 11 | packages: read 12 | contents: write 13 | 14 | jobs: 15 | announce: 16 | name: Announcements 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout main 20 | uses: actions/checkout@v4.1.1 21 | with: 22 | ref: "main" 23 | token: ${{ secrets.RELEASE_TOKEN }} 24 | - name: Release notes 25 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 26 | with: 27 | entrypoint: publish-release-notes-to-github 28 | env: 29 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 30 | - name: Zulip 31 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 32 | with: 33 | entrypoint: send-announcement-to-pony-zulip 34 | env: 35 | ZULIP_API_KEY: ${{ secrets.ZULIP_RELEASE_API_KEY }} 36 | ZULIP_EMAIL: ${{ secrets.ZULIP_RELEASE_EMAIL }} 37 | - name: Last Week in Pony 38 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 39 | with: 40 | entrypoint: add-announcement-to-last-week-in-pony 41 | env: 42 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 43 | 44 | post-announcement: 45 | name: Tasks to run after the release has been announced 46 | needs: 47 | - announce 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout main 51 | uses: actions/checkout@v4.1.1 52 | with: 53 | ref: "main" 54 | token: ${{ secrets.RELEASE_TOKEN }} 55 | - name: Rotate release notes 56 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 57 | with: 58 | entrypoint: rotate-release-notes 59 | env: 60 | GIT_USER_NAME: "Ponylang Main Bot" 61 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 62 | - name: Delete announcement trigger tag 63 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 64 | with: 65 | entrypoint: delete-announcement-tag 66 | env: 67 | GIT_USER_NAME: "Ponylang Main Bot" 68 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 69 | -------------------------------------------------------------------------------- /postgres/_backend_messages.pony: -------------------------------------------------------------------------------- 1 | class val _AuthenticationMD5PasswordMessage 2 | """ 3 | Message from the backend that indicates that MD5 authentication is being 4 | requested by the server. Contains a salt needed to construct the reply. 5 | """ 6 | let salt: String 7 | 8 | new val create(salt': String) => 9 | salt = salt' 10 | 11 | primitive _AuthenticationOkMessage 12 | """ 13 | Message from the backend that indicates that a session has been successfully 14 | authenticated. 15 | """ 16 | 17 | class val _CommandCompleteMessage 18 | """ 19 | Messagr from the backend that indicates that a command has finished running. 20 | The message contains information about final details of the command. 21 | """ 22 | let id: String 23 | let value: USize 24 | 25 | new val create(id': String, value': USize) => 26 | id = id' 27 | value = value' 28 | 29 | class val _DataRowMessage 30 | """ 31 | Message from the backend that represents a row of data from something like a 32 | SELECT. 33 | """ 34 | let columns: Array[(String|None)] val 35 | 36 | new val create(columns': Array[(String|None)] val) => 37 | columns = columns' 38 | 39 | primitive _EmptyQueryResponseMessage 40 | """ 41 | Message from the backend that acknowledges the receipt of an empty query. 42 | """ 43 | 44 | class val _ReadyForQueryMessage 45 | """ 46 | Message from the backend that indicates that it is ready for a new query 47 | cycle. Also sent as the last step in the session start up process. 48 | """ 49 | let _status: U8 50 | 51 | new val create(status: U8) => 52 | _status = status 53 | 54 | fun val idle(): Bool => 55 | """ 56 | Returns true if the backend status is "idle" 57 | """ 58 | _status == 'I' 59 | 60 | fun val in_transaction_block(): Bool => 61 | """ 62 | Returns true if the backend is executing a transaction 63 | """ 64 | _status == 'T' 65 | 66 | fun val failed_transaction(): Bool => 67 | """ 68 | Returns true if the backend is in a failed transaction block. Queries will 69 | be rejected until the transaction has ended. 70 | """ 71 | _status == 'E' 72 | 73 | class val _RowDescriptionMessage 74 | """ 75 | Message from the backend that contains metadata like field names for any 76 | forthcoming DataRowMessages. 77 | """ 78 | let columns: Array[(String, U32)] val 79 | 80 | new val create(columns': Array[(String, U32)] val) => 81 | columns = columns' 82 | 83 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | concurrency: release 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | packages: read 15 | 16 | jobs: 17 | # validation to assure that we should in fact continue with the release should 18 | # be done here. the primary reason for this step is to verify that the release 19 | # was started correctly by pushing a `release-X.Y.Z` tag rather than `X.Y.Z`. 20 | pre-artefact-creation: 21 | name: Tasks to run before artefact creation 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout main 25 | uses: actions/checkout@v4.1.1 26 | with: 27 | ref: "main" 28 | token: ${{ secrets.RELEASE_TOKEN }} 29 | - name: Validate CHANGELOG 30 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 31 | with: 32 | entrypoint: pre-artefact-changelog-check 33 | 34 | generate-documentation: 35 | name: Generate documentation for release 36 | environment: 37 | name: github-pages 38 | url: ${{ steps.deployment.outputs.page_url }} 39 | runs-on: ubuntu-latest 40 | needs: 41 | - pre-artefact-creation 42 | container: 43 | image: ghcr.io/ponylang/library-documentation-action-v2-insiders:release 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4.1.1 47 | with: 48 | ref: "main" 49 | token: ${{ secrets.RELEASE_TOKEN }} 50 | - name: Generate documentation 51 | run: /entrypoint.py 52 | env: 53 | INPUT_SITE_URL: "https://ponylang.github.io/postgres/" 54 | INPUT_LIBRARY_NAME: "postgres" 55 | INPUT_DOCS_BUILD_DIR: "build/postgres-docs" 56 | - name: Setup Pages 57 | uses: actions/configure-pages@v5 58 | - name: Upload artifact 59 | uses: actions/upload-pages-artifact@v3 60 | with: 61 | path: 'build/postgres-docs/site/' 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | 66 | trigger-release-announcement: 67 | name: Trigger release announcement 68 | runs-on: ubuntu-latest 69 | needs: 70 | - generate-documentation 71 | steps: 72 | - uses: actions/checkout@v4.1.1 73 | with: 74 | ref: "main" 75 | token: ${{ secrets.RELEASE_TOKEN }} 76 | - name: Trigger 77 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 78 | with: 79 | entrypoint: trigger-release-announcement 80 | env: 81 | GIT_USER_NAME: "Ponylang Main Bot" 82 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Postgres 2 | 3 | Pure Pony Postgres driver 4 | 5 | ## Status 6 | 7 | Postgres is an alpha-level package. 8 | 9 | We welcome users who are willing to experience errors and possible application shutdowns. Your feedback on API usage and in reporting bugs is greatly appreciated. 10 | 11 | Please note that if this library encounters a state that the programmers thought was impossible to hit, it will exit the program immediately with informational messages. Normal errors are handled in standard Pony fashion. 12 | 13 | ## Installation 14 | 15 | * Install [corral](https://github.com/ponylang/corral) 16 | * `corral add github.com/ponylang/postgres.git --version 0.2.2` 17 | * `corral fetch` to fetch your dependencies 18 | * `use "postgres"` to include this package 19 | * `corral run -- ponyc` to compile your application 20 | 21 | This library has a transitive dependency on [ponylang/net_ssl](https://github.com/ponylang/net_ssl). It requires a C SSL library to be installed. Please see the [net_ssl installation instructions](https://github.com/ponylang/net_ssl?tab=readme-ov-file#installation) for more information. 22 | 23 | ## API Documentation 24 | 25 | [https://ponylang.github.io/postgres](https://ponylang.github.io/postgres) 26 | 27 | ## Postgres API Support 28 | 29 | This library aims to support the Postgres API to the level required to use Postgres from Pony in ways that the Pony community needs. We do not aim to support the entire API surface. If there is functionality missing, we will happily accept high-quality pull requests to add additional support so long as they don't come with additional external dependencies or overly burdensome maintenance. 30 | 31 | ### Authentication 32 | 33 | Only MD5 password authentication is supported. KerberosV5, cleartext, SCM, GSS, SSPI, and SASL authentication methods are not supported. 34 | 35 | ### Commands 36 | 37 | Basic API commands related to querying are supported at this time. Some functionality that isn't yet supported is: 38 | 39 | * Supplying connection configuration to the server 40 | * Prepared statements (aka Extended Queries) 41 | * Pipelining queries 42 | * Function calls 43 | * COPY operations 44 | * Cancelling in progress requests 45 | * Session encryption 46 | 47 | Note the appearance of an item on the above list isn't a guarantee that it will be supported in the future. 48 | 49 | ### Data Types 50 | 51 | The following data types are fully supported and will be converted from their postgres type to the corresponding Pony type. All other data types will be presented as `String`. 52 | 53 | * `bool` => `Bool` 54 | * `int2` => `I16` 55 | * `int4` => `I32` 56 | * `int8` => `I64` 57 | * `float4` => `F32` 58 | * `float8` => `F64` 59 | 60 | As `String` is our default type, all character types such as `text` are returned to the user as `String` and as such, aren't listed in our supported types. 61 | -------------------------------------------------------------------------------- /postgres/_frontend_message.pony: -------------------------------------------------------------------------------- 1 | primitive _FrontendMessage 2 | fun startup(user: String, database: String): Array[U8] val => 3 | try 4 | recover val 5 | // 4 + 4 + 4 + 1 + user.size() + 1 + 8 + 1 + database.size() + 1 + 1 6 | let length = 25 + user.size() + database.size() 7 | let msg: Array[U8] = Array[U8].init(0, length) 8 | ifdef bigendian then 9 | msg.update_u32(0, length.u32())? 10 | else 11 | msg.update_u32(0, length.u32().bswap())? 12 | end 13 | 14 | // Add version numbers. 15 | // The version numbers are in network byte order, thus the endian check. 16 | ifdef bigendian then 17 | msg.update_u16(4, U16(3))? // Major Version Number 18 | msg.update_u16(6, U16(0))? // Minor Version Number 19 | else 20 | msg.update_u16(4, U16(3).bswap())? // Major Version Number 21 | msg.update_u16(6, U16(0).bswap())? // Minor Version Number 22 | end 23 | 24 | msg.copy_from("user".array(), 0, 8, 4) 25 | // space for null left here at byte 13 26 | msg.copy_from(user.array(), 0, 13, user.size()) 27 | // space for null left here at byte 13 + user.size() + 1 28 | 29 | msg.copy_from("database".array(), 0, 14 + user.size(), 8) 30 | // space for null left here at byte 14 + user.size() + 8 + 1 31 | msg.copy_from(database.array(), 0, 23 + user.size(), database.size()) 32 | // space for null left here at 33 | // space for null left here at 34 | msg 35 | end 36 | else 37 | _Unreachable() 38 | [] 39 | end 40 | 41 | fun password(pwd: String): Array[U8] val => 42 | try 43 | recover val 44 | let payload_length = pwd.size().u32() + 5 45 | let msg_length = (payload_length + 1).usize() 46 | let msg: Array[U8] = Array[U8].init(0, msg_length) 47 | msg.update_u8(0, 'p')? 48 | ifdef bigendian then 49 | msg.update_u32(1, payload_length)? 50 | else 51 | msg.update_u32(1, payload_length.bswap())? 52 | end 53 | msg.copy_from(pwd.array(), 0, 5, pwd.size()) 54 | // space for null left here 55 | msg 56 | end 57 | else 58 | _Unreachable() 59 | [] 60 | end 61 | 62 | fun query(string: String): Array[U8] val => 63 | try 64 | recover val 65 | // 1 + 4 + string.size() 66 | let payload_length = string.size().u32() + 5 67 | let msg_length = (payload_length + 1).usize() 68 | let msg: Array[U8] = Array[U8].init(0, msg_length) 69 | msg.update_u8(0, _MessageType.query())? 70 | ifdef bigendian then 71 | msg.update_u32(1, payload_length)? 72 | else 73 | msg.update_u32(1, payload_length.bswap())? 74 | end 75 | msg.copy_from(string.array(), 0, 5, string.size()) 76 | // space for null left here 77 | msg 78 | end 79 | else 80 | _Unreachable() 81 | [] 82 | end 83 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | config ?= release 2 | 3 | PACKAGE := postgres 4 | GET_DEPENDENCIES_WITH := corral fetch 5 | CLEAN_DEPENDENCIES_WITH := corral clean 6 | COMPILE_WITH := corral run -- ponyc 7 | 8 | BUILD_DIR ?= build/$(config) 9 | COVERAGE_DIR ?= build/coverage 10 | SRC_DIR ?= $(PACKAGE) 11 | EXAMPLES_DIR := examples 12 | coverage_binary := $(COVERAGE_DIR)/$(PACKAGE) 13 | tests_binary := $(BUILD_DIR)/$(PACKAGE) 14 | docs_dir := build/$(PACKAGE)-docs 15 | 16 | ifdef config 17 | ifeq (,$(filter $(config),debug release)) 18 | $(error Unknown configuration "$(config)") 19 | endif 20 | endif 21 | 22 | ifeq ($(config),release) 23 | PONYC = $(COMPILE_WITH) 24 | else 25 | PONYC = $(COMPILE_WITH) --debug 26 | endif 27 | 28 | ifeq (,$(filter $(MAKECMDGOALS),clean docs realclean start-pg-container stop-pg-container TAGS)) 29 | ifeq ($(ssl), 3.0.x) 30 | SSL = -Dopenssl_3.0.x 31 | else ifeq ($(ssl), 1.1.x) 32 | SSL = -Dopenssl_1.1.x 33 | else ifeq ($(ssl), 0.9.0) 34 | SSL = -Dopenssl_0.9.0 35 | else 36 | $(error Unknown SSL version "$(ssl)". Must set using 'ssl=FOO') 37 | endif 38 | endif 39 | 40 | PONYC := $(PONYC) $(SSL) 41 | 42 | SOURCE_FILES := $(shell find $(SRC_DIR) -name *.pony) 43 | EXAMPLES := $(notdir $(shell find $(EXAMPLES_DIR)/* -type d)) 44 | EXAMPLES_SOURCE_FILES := $(shell find $(EXAMPLES_DIR) -name *.pony) 45 | EXAMPLES_BINARIES := $(addprefix $(BUILD_DIR)/,$(EXAMPLES)) 46 | 47 | test: unit-tests integration-tests build-examples 48 | 49 | unit-tests: $(tests_binary) 50 | $^ --exclude=integration/ --sequential 51 | 52 | integration-tests: $(tests_binary) 53 | $^ --only=integration/ --sequential 54 | 55 | $(tests_binary): $(SOURCE_FILES) | $(BUILD_DIR) 56 | $(GET_DEPENDENCIES_WITH) 57 | $(PONYC) -o $(BUILD_DIR) $(SRC_DIR) 58 | 59 | build-examples: $(EXAMPLES_BINARIES) 60 | 61 | $(EXAMPLES_BINARIES): $(BUILD_DIR)/%: $(SOURCE_FILES) $(EXAMPLES_SOURCE_FILES) | $(BUILD_DIR) 62 | $(GET_DEPENDENCIES_WITH) 63 | $(PONYC) -o $(BUILD_DIR) $(EXAMPLES_DIR)/$* 64 | 65 | clean: 66 | $(CLEAN_DEPENDENCIES_WITH) 67 | rm -rf $(BUILD_DIR) 68 | rm -rf $(COVERAGE_DIR) 69 | 70 | $(docs_dir): $(SOURCE_FILES) 71 | rm -rf $(docs_dir) 72 | $(GET_DEPENDENCIES_WITH) 73 | $(PONYC) --docs-public --pass=docs --output build $(SRC_DIR) 74 | 75 | docs: $(docs_dir) 76 | 77 | TAGS: 78 | ctags --recurse=yes $(SRC_DIR) 79 | 80 | coverage: $(coverage_binary) 81 | kcov --include-pattern="/$(SRC_DIR)/" --exclude-pattern="/test/,_test.pony" $(COVERAGE_DIR) $(coverage_binary) 82 | 83 | $(coverage_binary): $(SOURCE_FILES) | $(COVERAGE_DIR) 84 | $(GET_DEPENDENCIES_WITH) 85 | $(PONYC) --debug -o $(COVERAGE_DIR) $(SRC_DIR) 86 | 87 | start-pg-container: 88 | @docker run --name pg -e POSTGRES_DB=postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_HOST_AUTH_METHOD=md5 -e POSTGRES_INITDB_ARGS="--auth-host=md5" -p 5432:5432 -d postgres:14.5 89 | 90 | stop-pg-container: 91 | @docker stop pg 92 | @docker rm pg 93 | 94 | all: test 95 | 96 | $(BUILD_DIR): 97 | mkdir -p $(BUILD_DIR) 98 | 99 | $(COVERAGE_DIR): 100 | mkdir -p $(COVERAGE_DIR) 101 | 102 | .PHONY: all build-examples clean docs TAGS test coverage start-pg-container stop-pg-container 103 | -------------------------------------------------------------------------------- /postgres/error_response_message.pony: -------------------------------------------------------------------------------- 1 | class val ErrorResponseMessage 2 | let severity: String 3 | let localized_severity: (String | None) 4 | let code: String 5 | let message: String 6 | let detail: (String | None) 7 | let hint: (String | None) 8 | let position: (String | None) 9 | let internal_position: (String | None) 10 | let internal_query: (String | None) 11 | let error_where: (String | None) 12 | let schema_name: (String | None) 13 | let table_name: (String | None) 14 | let column_name: (String | None) 15 | let data_type_name: (String | None) 16 | let constraint_name: (String | None) 17 | let file: (String | None) 18 | let line: (String | None) 19 | let routine: (String | None) 20 | 21 | new val create(severity': String, 22 | localized_severity': (String | None), 23 | code': String, 24 | message': String, 25 | detail': (String | None), 26 | hint': (String | None), 27 | position': (String | None), 28 | internal_position': (String | None), 29 | internal_query': (String | None), 30 | error_where': (String | None), 31 | schema_name': (String | None), 32 | table_name': (String | None), 33 | column_name': (String | None), 34 | data_type_name': (String | None), 35 | constraint_name': (String | None), 36 | file': (String | None), 37 | line': (String | None), 38 | routine': (String | None)) 39 | => 40 | severity = severity' 41 | localized_severity = localized_severity' 42 | code = code' 43 | message = message' 44 | detail = detail' 45 | hint = hint' 46 | position = position' 47 | internal_position = internal_position' 48 | internal_query = internal_query' 49 | error_where = error_where' 50 | schema_name = schema_name' 51 | table_name = table_name' 52 | column_name = column_name' 53 | data_type_name = data_type_name' 54 | constraint_name = constraint_name' 55 | file = file' 56 | line = line' 57 | routine = routine' 58 | 59 | class _ErrorResponseMessageBuilder 60 | var severity: (String | None) = None 61 | var localized_severity: (String | None) = None 62 | var code: (String | None) = None 63 | var message: (String | None) = None 64 | var detail: (String | None) = None 65 | var hint: (String | None) = None 66 | var position: (String | None) = None 67 | var internal_position: (String | None) = None 68 | var internal_query: (String | None) = None 69 | var error_where: (String | None) = None 70 | var schema_name: (String | None) = None 71 | var table_name: (String | None) = None 72 | var column_name: (String | None) = None 73 | var data_type_name: (String | None) = None 74 | var constraint_name: (String | None) = None 75 | var file: (String | None) = None 76 | var line: (String | None) = None 77 | var routine: (String | None) = None 78 | 79 | new create() => 80 | None 81 | 82 | fun ref build(): ErrorResponseMessage ? => 83 | // Three fields are required to build. All others are optional. 84 | let s = severity as String 85 | let c = code as String 86 | let m = message as String 87 | 88 | ErrorResponseMessage(s, 89 | localized_severity, 90 | c, 91 | m, 92 | detail, 93 | hint, 94 | position, 95 | internal_position, 96 | internal_query, 97 | error_where, 98 | schema_name, 99 | table_name, 100 | column_name, 101 | data_type_name, 102 | constraint_name, 103 | file, 104 | line, 105 | routine) 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /.github/workflows/prepare-for-a-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare for a release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'release-[0-9]+.[0-9]+.[0-9]+' 7 | 8 | concurrency: prepare-for-a-release 9 | 10 | permissions: 11 | packages: read 12 | contents: write 13 | 14 | jobs: 15 | # all tasks that need to be done before we add an X.Y.Z tag 16 | # should be done as a step in the pre-tagging job. 17 | # think of it like this... if when you later checkout the tag for a release, 18 | # should the change be there? if yes, do it here. 19 | pre-tagging: 20 | name: Tasks run before tagging the release 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout main 24 | uses: actions/checkout@v4.1.1 25 | with: 26 | ref: "main" 27 | token: ${{ secrets.RELEASE_TOKEN }} 28 | - name: Update CHANGELOG.md 29 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 30 | with: 31 | entrypoint: update-changelog-for-release 32 | env: 33 | GIT_USER_NAME: "Ponylang Main Bot" 34 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 35 | - name: Update VERSION 36 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 37 | with: 38 | entrypoint: update-version-in-VERSION 39 | env: 40 | GIT_USER_NAME: "Ponylang Main Bot" 41 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 42 | - name: Update version in README 43 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 44 | with: 45 | entrypoint: update-version-in-README 46 | env: 47 | GIT_USER_NAME: "Ponylang Main Bot" 48 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 49 | - name: Update corral.json 50 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 51 | with: 52 | entrypoint: update-version-in-corral-json 53 | env: 54 | GIT_USER_NAME: "Ponylang Main Bot" 55 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 56 | 57 | # tag for release 58 | # this will kick off the next stage of the release process 59 | # no additional steps should be added to this job 60 | tag-release: 61 | name: Tag the release 62 | needs: 63 | - pre-tagging 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Checkout main 67 | uses: actions/checkout@v4.1.1 68 | with: 69 | ref: "main" 70 | token: ${{ secrets.RELEASE_TOKEN }} 71 | - name: Trigger artefact creation 72 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 73 | with: 74 | entrypoint: trigger-artefact-creation 75 | env: 76 | GIT_USER_NAME: "Ponylang Main Bot" 77 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 78 | 79 | # all cleanup tags that should happen after tagging for release should happen 80 | # in the post-tagging job. examples of things you might do: 81 | # add a new unreleased section to a changelog 82 | # set a version back to "snapshot" 83 | # in general, post-tagging is for "going back to normal" from tasks that were 84 | # done during the pre-tagging job 85 | post-tagging: 86 | name: Tasks run after tagging the release 87 | needs: 88 | - tag-release 89 | runs-on: ubuntu-latest 90 | steps: 91 | - name: Checkout main 92 | uses: actions/checkout@v4.1.1 93 | with: 94 | ref: "main" 95 | token: ${{ secrets.RELEASE_TOKEN }} 96 | - name: Add "unreleased" section to CHANGELOG.md 97 | uses: docker://ghcr.io/ponylang/release-bot-action:0.6.3 98 | with: 99 | entrypoint: add-unreleased-section-to-changelog 100 | env: 101 | GIT_USER_NAME: "Ponylang Main Bot" 102 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainers at coc@ponylang.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | 48 | # Social Rules 49 | 50 | In addition to having a code of conduct as an anti-harassment policy, we have a small set of [social rules](https://www.recurse.com/manual#sub-sec-social-rules) we follow. We (the project maintainers) lifted these rules from the [Recurse Center](https://www.recurse.com). We've seen these rules in effect in other environments. We'd like the Pony community to share a similar positive environment. These rules are intended to be lightweight, and to make more explicit certain social norms that are normally implicit. Most of our social rules really boil down to “don't be a jerk” or “don't be annoying.” Of course, almost nobody sets out to be a jerk or annoying, so telling people not to be jerks isn't a very productive strategy. 51 | 52 | Unlike the anti-harassment policy, violation of the social rules will not result in expulsion from the Pony community or a strong warning from project maintainers. Rather, they are designed to provide some lightweight social structure for community members to use when interacting with each other. 53 | 54 | ## No feigning surprise. 55 | 56 | The first rule means you shouldn't act surprised when people say they don't know something. This applies to both technical things ("What?! I can't believe you don't know what the stack is!") and non-technical things ("You don't know who RMS is?!"). Feigning surprise has absolutely no social or educational benefit: When people feign surprise, it's usually to make them feel better about themselves and others feel worse. And even when that's not the intention, it's almost always the effect. 57 | 58 | ## No well-actually's 59 | 60 | A well-actually happens when someone says something that's almost - but not entirely - correct, and you say, "well, actually…" and then give a minor correction. This is especially annoying when the correction has no bearing on the actual conversation. This doesn't mean we aren't about truth-seeking or that we don't care about being precise. Almost all well-actually's in our experience are about grandstanding, not truth-seeking. 61 | 62 | ## No subtle -isms 63 | 64 | Our last social rule bans subtle racism, sexism, homophobia, transphobia, and other kinds of bias. This one is different from the rest, because it covers a class of behaviors instead of one very specific pattern. 65 | 66 | Subtle -isms are small things that make others feel uncomfortable, things that we all sometimes do by mistake. For example, saying "It's so easy my grandmother could do it" is a subtle -ism. Like the other three social rules, this one is often accidentally broken. Like the other three, it's not a big deal to mess up – you just apologize and move on. 67 | 68 | If you see a subtle -ism in the Pony community, you can point it out to the relevant person, either publicly or privately, or you can ask one of the project maintainers to say something. After this, we ask that all further discussion move off of public channels. If you are a third party, and you don't see what could be biased about the comment that was made, feel free to talk to the project maintainers. Please don't say, "Comment X wasn't homophobic!" Similarly, please don't pile on to someone who made a mistake. The "subtle" in "subtle -isms" means that it's probably not obvious to everyone right away what was wrong with the comment. 69 | 70 | If you have any questions about any part of the code of conduct or social rules, please feel free to reach out to any of the project maintainers. 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You want to contribute to postgres? Awesome. 4 | 5 | There are a number of ways to contribute. As this document is a little long, feel free to jump to the section that applies to you currently: 6 | 7 | * [Bug report](#bug-report) 8 | * [How to contribute](#how-to-contribute) 9 | * [Pull request](#pull-request) 10 | 11 | Additional notes regarding formatting: 12 | 13 | * [Documentation formatting](#documentation-formatting) 14 | * [Code formatting](#code-formatting) 15 | * [File Naming](#standard-library-file-naming) 16 | 17 | ## Bug report 18 | 19 | First of all please [search existing issues](https://github.com/ponylang/postgres/issues) to make sure your issue hasn't already been reported. If you cannot find a suitable issue — [create a new one](https://github.com/ponylang/postgres/issues/new). 20 | 21 | Provide the following details: 22 | 23 | * short summary of what you were trying to achieve, 24 | * a code snippet causing the bug, 25 | * expected result, 26 | * actual results and 27 | * environment details: at least operating system version 28 | 29 | If possible, try to isolate the problem and provide just enough code to demonstrate it. Add any related information which might help to fix the issue. 30 | 31 | ## How to Contribute 32 | 33 | This project uses a fairly standard GitHub pull request workflow. If you have already contributed to a project via GitHub pull request, you can skip this section and proceed to the [specific details of what we ask for in a pull request](#pull-request). If this is your first time contributing to a project via GitHub, read on. 34 | 35 | Here is the basic GitHub workflow: 36 | 37 | 1. Fork this repo. you can do this via the GitHub website. This will result in you having your own copy of the repo under your GitHub account. 38 | 2. Clone your forked repo to your local machine 39 | 3. Make a branch for your change 40 | 4. Make your change on that branch 41 | 5. Push your change to your repo 42 | 6. Use the github ui to open a PR 43 | 44 | Some things to note that aren't immediately obvious to folks just starting out: 45 | 46 | 1. Your fork doesn't automatically stay up to date with changes in the main repo. 47 | 2. Any changes you make on your branch that you used for one PR will automatically appear in another PR so if you have more than 1 PR, be sure to always create different branches for them. 48 | 3. Weird things happen with commit history if you don't create your PR branches off of `main` so always make sure you have the `main` branch checked out before creating a branch for a PR 49 | 50 | If you feel overwhelmed at any point, don't worry, it can be a lot to learn when you get started. You can usually find us in the [Pony Zulip](https://ponylang.zulipchat.com/) if you need help. 51 | 52 | You can get help using GitHub via [the official documentation](https://help.github.com/). Some hightlights include: 53 | 54 | * [Fork A Repo](https://help.github.com/articles/fork-a-repo/) 55 | * [Creating a pull request](https://help.github.com/articles/creating-a-pull-request/) 56 | * [Syncing a fork](https://help.github.com/articles/syncing-a-fork/) 57 | 58 | ## Pull request 59 | 60 | While we don't require that your pull request be a single commit, note that we will end up squashing all your commits into a single commit when we merge. While your PR is in review, we may ask for additional changes, please do not squash those commits while the review is underway. We ask that you not squash while a review is underway as it can make it hard to follow what is going on. 61 | 62 | When opening your pull request, please make sure that the initial comment on the PR is the commit message we should use when we merge. Making sure your commit message conforms to these guidelines for [writ(ing) a good commit message](http://chris.beams.io/posts/git-commit/). 63 | 64 | Make sure to issue 1 pull request per feature. Don't lump unrelated changes together. If you find yourself using the word "and" in your commit comment, you 65 | are probably doing too much for a single PR. 66 | 67 | We keep a [CHANGELOG](CHANGELOG.md) of all software changes with behavioural effects in ponyc. If your PR includes such changes (rather than say a documentation update), a Pony team member will do the following before merging it, so that the PR will be automatically added to the CHANGELOG: 68 | 69 | * Ensure that the ticket is tagged with one or more appropriate "changelog - *" labels - each label corresponds to a section of the changelog where this change will be automatically mentioned. 70 | * Ensure that the ticket title is appropriate - the title will be used as the summary of the change, so it should be appropriately formatted, including a ticket reference if the PR is a fix to an existing bug ticket. 71 | * For example, an appropriate title for a PR that fixes a bug reported in issue ticket #98 might look like: 72 | * *Fixed compiler crash related to tuple recovery (issue #98)* 73 | 74 | Once those conditions are met, the PR can be merged, and an automated system will immediately add the entry to the changelog. Keeping the changelog entries out of the file changes in the PR helps to avoid conflicts and other administrative headaches when many PRs are in progress. 75 | 76 | Any change that involves a changelog entry will trigger a bot to request that you add release notes to your PR. 77 | 78 | Pull requests from accounts that aren't members of the Ponylang organization require approval from a member before running. Approval is required after each update that you make. This could involve a lot of waiting on your part for approvals. If you are opening PRs to verify that changes all pass CI before "opening it for real", we strongly suggest that you open the PR against the `main` branch of your fork. CI will then run in your fork and you don't need to wait for approval from a Ponylang member. 79 | 80 | ## Documentation formatting 81 | 82 | When contributing to documentation, try to keep the following style guidelines in mind: 83 | 84 | As much as possible all documentation should be textual and in Markdown format. Diagrams are often needed to clarify a point. For any images, an original high-resolution source should be provided as well so updates can be made. 85 | 86 | Documentation is not "source code." As such, it should not be wrapped at 80 columns. Documentation should be allowed to flow naturally until the end of a paragraph. It is expected that the reader will turn on soft wrapping as needed. 87 | 88 | All code examples in documentation should be formatted in a fashion appropriate to the language in question. 89 | 90 | All command line examples in documentation should be presented in a copy and paste friendly fashion. Assume the user is using the `bash` shell. GitHub formatting on long command lines can be unfriendly to copy-and-paste. Long command lines should be broken up using `\` so that each line is no more than 80 columns. Wrapping at 80 columns should result in a good display experience in GitHub. Additionally, continuation lines should be indented two spaces. 91 | 92 | OK: 93 | 94 | ```bash 95 | my_command --some-option foo --path-to-file ../../wallaroo/long/line/foo \ 96 | --some-other-option bar 97 | ``` 98 | 99 | Not OK: 100 | 101 | ```bash 102 | my_command --some-option foo --path-to-file ../../wallaroo/long/line/foo --some-other-option bar 103 | ``` 104 | 105 | Wherever possible when writing documentation, favor full command options rather than short versions. Full flags are usually much easier to modify because the meaning is clearer. 106 | 107 | OK: 108 | 109 | ```bash 110 | my_command --messages 100 111 | ``` 112 | 113 | Not OK: 114 | 115 | ```bash 116 | my_command -m 100 117 | ``` 118 | 119 | ## Code formatting 120 | 121 | The basics: 122 | 123 | * Indentation 124 | 125 | Indent using spaces, not tabs. Indentation is language specific. 126 | 127 | * Watch your whitespace! 128 | 129 | Use an editor plugin to remove unused trailing whitespace including both at the end of a line and at the end of a file. By the same token, remember to leave a single newline only line at the end of each file. It makes output files to the console much more pleasant. 130 | 131 | * Line Length 132 | 133 | We all have different sized monitors. What might look good on yours might look like awful on another. Be kind and wrap all lines at 80 columns unless you have a good reason not to. 134 | 135 | * Reformatting code to meet standards 136 | 137 | Try to avoid doing it. A commit that changes the formatting for large chunks of a file makes for an ugly commit history when looking for changes. Don't commit code that doesn't conform to coding standards in the first place. If you do reformat code, make sure it is either standalone reformatting with no logic changes or confined solely to code whose logic you touched. For example, updating the indentation in a file? Do not make logic changes along with it. Editing a line that has extra whitespace at the end? Feel free to remove it. 138 | 139 | The details: 140 | 141 | All Pony sources should follow the [Pony standard library style guide](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md). 142 | 143 | ## File naming 144 | 145 | Pony code follows the [Pony standard library file naming guidelines](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md#naming). 146 | -------------------------------------------------------------------------------- /postgres/_response_parser.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use "collections" 3 | 4 | type _AuthenticationMessages is 5 | ( _AuthenticationOkMessage 6 | | _AuthenticationMD5PasswordMessage ) 7 | 8 | type _ResponseParserResult is 9 | ( _AuthenticationMessages 10 | | _CommandCompleteMessage 11 | | _DataRowMessage 12 | | _EmptyQueryResponseMessage 13 | | _ReadyForQueryMessage 14 | | _RowDescriptionMessage 15 | | _UnsupportedMessage 16 | | ErrorResponseMessage 17 | | None ) 18 | 19 | primitive _UnsupportedMessage 20 | 21 | primitive _ResponseParser 22 | """ 23 | Takes a reader that contains buffered responses from a Postgres server and 24 | extract a single message. To process a full buffer, `apply` should be called in a loop until it returns `None` rather than a message type. The input 25 | buffer is modified. 26 | 27 | Throws an error if an unrecoverable error is encountered. The session should 28 | be shut down in response. 29 | """ 30 | fun apply(buffer: Reader): _ResponseParserResult ? => 31 | // The minimum size for any complete message is 5. If we have less than 32 | // 5 received bytes buffered than there is no point to continuing as we 33 | // definitely don't have a full message. 34 | if buffer.size() < 5 then 35 | return None 36 | end 37 | 38 | let message_type = buffer.peek_u8(0)? 39 | if ((message_type < 'A') or (message_type > 'z')) or 40 | ((message_type > 'Z') and (message_type < 'a')) 41 | then 42 | // All message codes are ascii letters. If we get something that isn't 43 | // one then we know we have junk. 44 | error 45 | end 46 | 47 | // postgres sends the payload size as the payload plus the 4 bytes for the 48 | // descriptive header on the payload. We are calling `payload_size` to be 49 | // only the payload, not the header as well. 50 | let payload_size = buffer.peek_u32_be(1)?.usize() - 4 51 | let message_size = payload_size + 4 + 1 52 | 53 | // The message will be `message_size` in length. If we have less than 54 | // that then there's no point in continuing. 55 | if buffer.size() < message_size then 56 | return None 57 | end 58 | 59 | match message_type 60 | | _MessageType.authentication_request() => 61 | let auth_type = buffer.peek_i32_be(5)? 62 | 63 | if auth_type == _AuthenticationRequestType.ok() then 64 | // discard the message and type header 65 | buffer.skip(message_size)? 66 | // notify that we are authenticated 67 | return _AuthenticationOkMessage 68 | elseif auth_type == _AuthenticationRequestType.md5_password() then 69 | let salt = String.from_array( 70 | recover val 71 | [ buffer.peek_u8(9)? 72 | buffer.peek_u8(10)? 73 | buffer.peek_u8(11)? 74 | buffer.peek_u8(12)? ] 75 | end) 76 | // discard the message now that we've extracted the salt. 77 | buffer.skip(message_size)? 78 | 79 | return _AuthenticationMD5PasswordMessage(salt) 80 | else 81 | buffer.skip(message_size)? 82 | return _UnsupportedMessage 83 | end 84 | | _MessageType.error_response() => 85 | // Slide past the header... 86 | buffer.skip(5)? 87 | // and only get the payload 88 | let payload = buffer.block(payload_size)? 89 | return _error_response(consume payload)? 90 | | _MessageType.ready_for_query() => 91 | // Slide past the header... 92 | buffer.skip(5)? 93 | // and only get the status indicator byte 94 | return _ready_for_query(buffer.u8()?)? 95 | | _MessageType.command_complete() => 96 | // Slide past the header... 97 | buffer.skip(5)? 98 | // and only get the payload 99 | let payload = buffer.block(payload_size - 1)? 100 | // And skip the null terminator 101 | buffer.skip(1)? 102 | return _command_complete(consume payload)? 103 | | _MessageType.data_row() => 104 | // Slide past the header... 105 | buffer.skip(5)? 106 | // and only get the payload 107 | let payload = buffer.block(payload_size)? 108 | return _data_row(consume payload)? 109 | | _MessageType.row_description() => 110 | // Slide past the header... 111 | buffer.skip(5)? 112 | // and only get the payload 113 | let payload = buffer.block(payload_size)? 114 | return _row_description(consume payload)? 115 | | _MessageType.empty_query_response() => 116 | // Slide past the header... 117 | buffer.skip(5)? 118 | // and there's nothing else 119 | return _EmptyQueryResponseMessage 120 | else 121 | buffer.skip(message_size)? 122 | return _UnsupportedMessage 123 | end 124 | 125 | fun _error_response(payload: Array[U8] val): ErrorResponseMessage ? => 126 | """ 127 | Parse error response messages. 128 | """ 129 | var code = "" 130 | var code_index: USize = 0 131 | 132 | let builder = _ErrorResponseMessageBuilder 133 | while (payload(code_index)? > 0) do 134 | let field_type = payload(code_index)? 135 | 136 | // Find the field terminator. All fields are null terminated. 137 | let null_index = payload.find(0, code_index)? 138 | let field_index = code_index + 1 139 | let field_data = String.from_array(recover 140 | payload.slice(field_index, null_index) 141 | end) 142 | 143 | match field_type 144 | | 'S' => builder.severity = field_data 145 | | 'V' => builder.localized_severity = field_data 146 | | 'C' => builder.code = field_data 147 | | 'M' => builder.message = field_data 148 | | 'D' => builder.detail = field_data 149 | | 'H' => builder.hint = field_data 150 | | 'P' => builder.position = field_data 151 | | 'p' => builder.internal_position = field_data 152 | | 'q' => builder.internal_query = field_data 153 | | 'W' => builder.error_where = field_data 154 | | 's' => builder.schema_name = field_data 155 | | 't' => builder.table_name = field_data 156 | | 'c' => builder.column_name = field_data 157 | | 'd' => builder.data_type_name = field_data 158 | | 'n' => builder.constraint_name = field_data 159 | | 'F' => builder.file = field_data 160 | | 'L' => builder.line = field_data 161 | | 'R' => builder.line = field_data 162 | end 163 | 164 | code_index = null_index + 1 165 | end 166 | 167 | builder.build()? 168 | 169 | fun _data_row(payload: Array[U8] val): _DataRowMessage ? => 170 | """ 171 | Parse a data row message. 172 | """ 173 | let reader: Reader = Reader.>append(payload) 174 | let number_of_columns = reader.u16_be()?.usize() 175 | let columns: Array[(String| None)] iso = recover iso 176 | columns.create(number_of_columns) 177 | end 178 | 179 | for column_index in Range(0, number_of_columns) do 180 | let column_length = reader.u32_be()? 181 | match column_length 182 | | -1 => 183 | columns.push(None) 184 | | 0 => 185 | columns.push("") 186 | else 187 | let column = reader.block(column_length.usize())? 188 | let column_as_string = String.from_array(consume column) 189 | columns.push(column_as_string) 190 | end 191 | end 192 | 193 | _DataRowMessage(consume columns) 194 | 195 | fun _row_description(payload: Array[U8] val): _RowDescriptionMessage ? => 196 | """ 197 | Parse a row description message. 198 | """ 199 | let reader: Reader = Reader.>append(payload) 200 | let number_of_columns = reader.u16_be()?.usize() 201 | let columns: Array[(String, U32)] iso = recover iso 202 | columns.create(number_of_columns) 203 | end 204 | 205 | for column_index in Range(0, number_of_columns) do 206 | // column name is a null terminated string 207 | let cn = reader.read_until(0)? 208 | let column_name = String.from_array(consume cn) 209 | // skip table id (int32) and column attribute number (int16) 210 | reader.skip(6)? 211 | // column data type 212 | let column_data_type = reader.u32_be()? 213 | // skip remaining 3 fields int16, int32, int16 214 | reader.skip(8)? 215 | columns.push((column_name, column_data_type)) 216 | end 217 | 218 | _RowDescriptionMessage(consume columns) 219 | 220 | fun _ready_for_query(status: U8): _ReadyForQueryMessage ? => 221 | if (status == 'I') or 222 | (status == 'T') or 223 | (status == 'E') 224 | then 225 | _ReadyForQueryMessage(status) 226 | else 227 | error 228 | end 229 | 230 | fun _command_complete(payload: Array[U8] val): _CommandCompleteMessage ? => 231 | """ 232 | Parse a command complete message 233 | """ 234 | let id = String.from_array(payload) 235 | if id.size() == 0 then 236 | error 237 | end 238 | 239 | let parts = id.split(" ") 240 | match parts.size() 241 | | 1 => 242 | _CommandCompleteMessage(parts(0)?, 0) 243 | | 2 => 244 | let first = parts(0)? 245 | let second = parts(1)? 246 | try 247 | let value = second.u64()?.usize() 248 | _CommandCompleteMessage(first, value) 249 | else 250 | _CommandCompleteMessage(id, 0) 251 | end 252 | | 3 => 253 | if parts(0)? == "INSERT" then 254 | _CommandCompleteMessage(parts(0)?, parts(2)?.u64()?.usize()) 255 | else 256 | let first = parts(0)? 257 | let second = parts(1)? 258 | let third = parts(2)? 259 | let id' = recover val " ".join([first; second].values()) end 260 | try 261 | let value = third.u64()?.usize() 262 | _CommandCompleteMessage(id', value) 263 | else 264 | _CommandCompleteMessage(id', 0) 265 | end 266 | end 267 | else 268 | _CommandCompleteMessage(id, 0) 269 | end 270 | -------------------------------------------------------------------------------- /postgres/_test.pony: -------------------------------------------------------------------------------- 1 | use "cli" 2 | use "collections" 3 | use lori = "lori" 4 | use "pony_test" 5 | 6 | actor \nodoc\ Main is TestList 7 | new create(env: Env) => 8 | PonyTest(env, this) 9 | 10 | new make() => 11 | None 12 | 13 | fun tag tests(test: PonyTest) => 14 | test(_TestAuthenticate) 15 | test(_TestAuthenticateFailure) 16 | test(_TestConnect) 17 | test(_TestConnectFailure) 18 | test(_TestCreateAndDropTable) 19 | test(_TestEmptyQuery) 20 | test(_TestHandlingJunkMessages) 21 | test(_TestInsertAndDelete) 22 | test(_TestFrontendMessagePassword) 23 | test(_TestFrontendMessageQuery) 24 | test(_TestFrontendMessageStartup) 25 | test(_TestQueryAfterAuthenticationFailure) 26 | test(_TestQueryAfterConnectionFailure) 27 | test(_TestQueryAfterSessionHasBeenClosed) 28 | test(_TestQueryResults) 29 | test(_TestQueryOfNonExistentTable) 30 | test(_TestResponseParserAuthenticationMD5PasswordMessage) 31 | test(_TestResponseParserAuthenticationOkMessage) 32 | test(_TestResponseParserCommandCompleteMessage) 33 | test(_TestResponseParserDataRowMessage) 34 | test(_TestResponseParserEmptyBuffer) 35 | test(_TestResponseParserEmptyQueryResponseMessage) 36 | test(_TestResponseParserErrorResponseMessage) 37 | test(_TestResponseParserIncompleteMessage) 38 | test(_TestResponseParserJunkMessage) 39 | test(_TestResponseParserMultipleMessagesAuthenticationMD5PasswordFirst) 40 | test(_TestResponseParserMultipleMessagesAuthenticationOkFirst) 41 | test(_TestResponseParserMultipleMessagesErrorResponseFirst) 42 | test(_TestResponseParserReadyForQueryMessage) 43 | test(_TestResponseParserRowDescriptionMessage) 44 | test(_TestUnansweredQueriesFailOnShutdown) 45 | 46 | class \nodoc\ iso _TestAuthenticate is UnitTest 47 | """ 48 | Test to verify that given correct login information we can authenticate with 49 | a Postgres server. This test assumes that connecting is working correctly and 50 | will fail if it isn't. 51 | """ 52 | fun name(): String => 53 | "integration/Authenicate" 54 | 55 | fun apply(h: TestHelper) => 56 | let info = _ConnectionTestConfiguration(h.env.vars) 57 | 58 | let session = Session( 59 | lori.TCPConnectAuth(h.env.root), 60 | _AuthenticateTestNotify(h, true), 61 | info.host, 62 | info.port, 63 | info.username, 64 | info.password, 65 | info.database) 66 | 67 | h.dispose_when_done(session) 68 | h.long_test(5_000_000_000) 69 | 70 | class \nodoc\ iso _TestAuthenticateFailure is UnitTest 71 | """ 72 | Test to verify when we fail to authenticate with a Postgres server that are 73 | handling the failure correctly. This test assumes that connecting is working 74 | correctly and will fail if it isn't. 75 | """ 76 | fun name(): String => 77 | "integration/AuthenicateFailure" 78 | 79 | fun apply(h: TestHelper) => 80 | let info = _ConnectionTestConfiguration(h.env.vars) 81 | 82 | let session = Session( 83 | lori.TCPConnectAuth(h.env.root), 84 | _AuthenticateTestNotify(h, false), 85 | info.host, 86 | info.port, 87 | info.username, 88 | info.password + " " + info.password, 89 | info.database) 90 | 91 | h.dispose_when_done(session) 92 | h.long_test(5_000_000_000) 93 | 94 | actor \nodoc\ _AuthenticateTestNotify is SessionStatusNotify 95 | let _h: TestHelper 96 | let _success_expected: Bool 97 | 98 | new create(h: TestHelper, success_expected: Bool) => 99 | _h = h 100 | _success_expected = success_expected 101 | 102 | be pg_session_authenticated(session: Session) => 103 | _h.complete(_success_expected == true) 104 | 105 | be pg_session_authentication_failed( 106 | s: Session, 107 | reason: AuthenticationFailureReason) 108 | => 109 | _h.complete(_success_expected == false) 110 | 111 | class \nodoc\ iso _TestConnect is UnitTest 112 | """ 113 | Test to verify that given correct login information that we can connect to 114 | a Postgres server. 115 | """ 116 | fun name(): String => 117 | "integration/Connect" 118 | 119 | fun apply(h: TestHelper) => 120 | let info = _ConnectionTestConfiguration(h.env.vars) 121 | 122 | let session = Session( 123 | lori.TCPConnectAuth(h.env.root), 124 | _ConnectTestNotify(h, true), 125 | info.host, 126 | info.port, 127 | info.username, 128 | info.password, 129 | info.database) 130 | 131 | h.dispose_when_done(session) 132 | h.long_test(5_000_000_000) 133 | 134 | class \nodoc\ iso _TestConnectFailure is UnitTest 135 | """ 136 | Test to verify that connection failures are handled correctly. Currently, 137 | we set up a bad connect attempt by taking the valid port number that would 138 | allow a connect and reversing it to create an attempt to connect on a port 139 | that nothing should be listening on. 140 | """ 141 | fun name(): String => 142 | "integration/ConnectFailure" 143 | 144 | fun apply(h: TestHelper) => 145 | let info = _ConnectionTestConfiguration(h.env.vars) 146 | 147 | let session = Session( 148 | lori.TCPConnectAuth(h.env.root), 149 | _ConnectTestNotify(h, false), 150 | info.host, 151 | info.port.reverse(), 152 | info.username, 153 | info.password, 154 | info.database) 155 | 156 | h.dispose_when_done(session) 157 | h.long_test(5_000_000_000) 158 | 159 | actor \nodoc\ _ConnectTestNotify is SessionStatusNotify 160 | let _h: TestHelper 161 | let _success_expected: Bool 162 | 163 | new create(h: TestHelper, success_expected: Bool) => 164 | _h = h 165 | _success_expected = success_expected 166 | 167 | be pg_session_connected(session: Session) => 168 | _h.complete(_success_expected == true) 169 | 170 | be pg_session_connection_failed(session: Session) => 171 | _h.complete(_success_expected == false) 172 | 173 | class \nodoc\ val _ConnectionTestConfiguration 174 | let host: String 175 | let port: String 176 | let username: String 177 | let password: String 178 | let database: String 179 | 180 | new val create(vars: (Array[String] val | None)) => 181 | let e = EnvVars(vars) 182 | host = try e("POSTGRES_HOST")? else "127.0.0.1" end 183 | port = try e("POSTGRES_PORT")? else "5432" end 184 | username = try e("POSTGRES_USERNAME")? else "postgres" end 185 | password = try e("POSTGRES_PASSWORD")? else "postgres" end 186 | database = try e("POSTGRES_DATABASE")? else "postgres" end 187 | 188 | class \nodoc\ iso _TestHandlingJunkMessages is UnitTest 189 | """ 190 | Verifies that a session shuts down when receiving junk from the server. 191 | """ 192 | fun name(): String => 193 | "HandlingJunkMessages" 194 | 195 | fun apply(h: TestHelper) => 196 | let host = "127.0.0.1" 197 | let port = "7669" 198 | 199 | let listener = _JunkSendingTestListener( 200 | lori.TCPListenAuth(h.env.root), 201 | host, 202 | port, 203 | h) 204 | 205 | h.dispose_when_done(listener) 206 | h.long_test(5_000_000_000) 207 | 208 | actor \nodoc\ _HandlingJunkTestNotify is SessionStatusNotify 209 | let _h: TestHelper 210 | 211 | new create(h: TestHelper) => 212 | _h = h 213 | 214 | be pg_session_shutdown(s: Session) => 215 | _h.complete(true) 216 | 217 | be pg_session_connection_failed(s: Session) => 218 | _h.fail("Unable to establish connection") 219 | _h.complete(false) 220 | 221 | actor \nodoc\ _JunkSendingTestListener is lori.TCPListenerActor 222 | """ 223 | Listens for incoming connections and starts a server that will always reply 224 | with junk. 225 | """ 226 | var _tcp_listener: lori.TCPListener = lori.TCPListener.none() 227 | let _server_auth: lori.TCPServerAuth 228 | let _h: TestHelper 229 | let _host: String 230 | let _port: String 231 | 232 | new create(listen_auth: lori.TCPListenAuth, 233 | host: String, 234 | port: String, 235 | h: TestHelper) 236 | => 237 | _host = host 238 | _port = port 239 | _h = h 240 | _server_auth = lori.TCPServerAuth(listen_auth) 241 | _tcp_listener = lori.TCPListener(listen_auth, host, port, this) 242 | 243 | fun ref _listener(): lori.TCPListener => 244 | _tcp_listener 245 | 246 | fun ref _on_accept(fd: U32): _JunkSendingTestServer => 247 | _JunkSendingTestServer(_server_auth, fd) 248 | 249 | fun ref _on_listening() => 250 | // Now that we are listening, start a client session 251 | Session( 252 | lori.TCPConnectAuth(_h.env.root), 253 | _HandlingJunkTestNotify(_h), 254 | _host, 255 | _port, 256 | "postgres", 257 | "postgres", 258 | "postgres") 259 | 260 | fun ref _on_listen_failure() => 261 | _h.fail("Unable to listen") 262 | _h.complete(false) 263 | 264 | actor \nodoc\ _JunkSendingTestServer 265 | is (lori.TCPConnectionActor & lori.ServerLifecycleEventReceiver) 266 | """ 267 | Sends junk "postgres messages" in reponse to any incoming activity. This actor 268 | is used to test that our client handles getting junk correctly. 269 | """ 270 | var _tcp_connection: lori.TCPConnection = lori.TCPConnection.none() 271 | 272 | new create(auth: lori.TCPServerAuth, fd: U32) => 273 | _tcp_connection = lori.TCPConnection.server(auth, fd, this, this) 274 | let junk = _IncomingJunkTestMessage.bytes() 275 | _tcp_connection.send(junk) 276 | 277 | fun ref _connection(): lori.TCPConnection => 278 | _tcp_connection 279 | 280 | fun ref _next_lifecycle_event_receiver(): None => 281 | None 282 | 283 | fun ref _on_received(data: Array[U8] iso) => 284 | let junk = _IncomingJunkTestMessage.bytes() 285 | _tcp_connection.send(junk) 286 | 287 | class \nodoc\ iso _TestUnansweredQueriesFailOnShutdown is UnitTest 288 | """ 289 | Verifies that when a sesison is shutting down, it sends "SessionClosed" query 290 | failures for any queries that are queued or haven't completed yet. 291 | """ 292 | fun name(): String => 293 | "UnansweredQueriesFailOnShutdown" 294 | 295 | fun apply(h: TestHelper) => 296 | let host = "127.0.0.1" 297 | let port = "9667" 298 | 299 | let listener = _DoesntAnswerTestListener( 300 | lori.TCPListenAuth(h.env.root), 301 | host, 302 | port, 303 | h) 304 | 305 | h.dispose_when_done(listener) 306 | h.long_test(5_000_000_000) 307 | 308 | actor \nodoc\ _DoesntAnswerClient is (SessionStatusNotify & ResultReceiver) 309 | let _h: TestHelper 310 | let _in_flight_queries: SetIs[SimpleQuery] = _in_flight_queries.create() 311 | 312 | new create(h: TestHelper) => 313 | _h = h 314 | 315 | be pg_session_connection_failed(s: Session) => 316 | _h.fail("Unable to establish connection.") 317 | _h.complete(false) 318 | 319 | be pg_session_authenticated(session: Session) => 320 | _send_query(session, "select * from free_candy") 321 | _send_query(session, "select * from expensive_candy") 322 | session.close() 323 | 324 | be pg_session_authentication_failed( 325 | session: Session, 326 | reason: AuthenticationFailureReason) 327 | => 328 | _h.fail("Unable to authenticate.") 329 | _h.complete(false) 330 | 331 | be pg_query_result(result: Result) => 332 | _h.fail("Unexpectedly got a result for a query.") 333 | _h.complete(false) 334 | 335 | be pg_query_failed(query: SimpleQuery, 336 | failure: (ErrorResponseMessage | ClientQueryError)) 337 | => 338 | if _in_flight_queries.contains(query) then 339 | match failure 340 | | SessionClosed => 341 | _in_flight_queries.unset(query) 342 | if _in_flight_queries.size() == 0 then 343 | _h.complete(true) 344 | end 345 | else 346 | _h.fail("Got an incorrect query failure reason.") 347 | _h.complete(false) 348 | end 349 | else 350 | _h.fail("Got a failure for a query we didn't send.") 351 | _h.complete(false) 352 | end 353 | 354 | fun ref _send_query(session: Session, string: String) => 355 | let q = SimpleQuery(string) 356 | _in_flight_queries.set(q) 357 | session.execute(q, this) 358 | 359 | actor \nodoc\ _DoesntAnswerTestListener is lori.TCPListenerActor 360 | """ 361 | Listens for incoming connections and starts a server that will never reply 362 | """ 363 | var _tcp_listener: lori.TCPListener = lori.TCPListener.none() 364 | let _server_auth: lori.TCPServerAuth 365 | let _h: TestHelper 366 | let _host: String 367 | let _port: String 368 | 369 | new create(listen_auth: lori.TCPListenAuth, 370 | host: String, 371 | port: String, 372 | h: TestHelper) 373 | => 374 | _host = host 375 | _port = port 376 | _h = h 377 | _server_auth = lori.TCPServerAuth(listen_auth) 378 | _tcp_listener = lori.TCPListener(listen_auth, host, port, this) 379 | 380 | fun ref _listener(): lori.TCPListener => 381 | _tcp_listener 382 | 383 | fun ref _on_accept(fd: U32): _DoesntAnswerTestServer => 384 | _DoesntAnswerTestServer(_server_auth, fd) 385 | 386 | fun ref _on_listening() => 387 | // Now that we are listening, start a client session 388 | Session( 389 | lori.TCPConnectAuth(_h.env.root), 390 | _DoesntAnswerClient(_h), 391 | _host, 392 | _port, 393 | "postgres", 394 | "postgres", 395 | "postgres") 396 | 397 | fun ref _on_listen_failure() => 398 | _h.fail("Unable to listen") 399 | _h.complete(false) 400 | 401 | actor \nodoc\ _DoesntAnswerTestServer 402 | is (lori.TCPConnectionActor & lori.ServerLifecycleEventReceiver) 403 | """ 404 | Eats all incoming messages. By not answering, it allows us to simulate what 405 | happens with unanswered commands. 406 | 407 | Will answer it's "first received message" with an auth ok message. 408 | """ 409 | var _authed: Bool = false 410 | var _tcp_connection: lori.TCPConnection = lori.TCPConnection.none() 411 | 412 | new create(auth: lori.TCPServerAuth, fd: U32) => 413 | _tcp_connection = lori.TCPConnection.server(auth, fd, this, this) 414 | 415 | fun ref _connection(): lori.TCPConnection => 416 | _tcp_connection 417 | 418 | fun ref _next_lifecycle_event_receiver(): None => 419 | None 420 | 421 | fun ref _on_received(data: Array[U8] iso) => 422 | """ 423 | We authenticate the user without needing to receive any password etc. 424 | You connect, you are authed! This makes dummy server setup much easier but 425 | it is possible that eventually this might trip us up. At the moment, this 426 | isn't problematic that we are "auto authing". 427 | """ 428 | if not _authed then 429 | _authed = true 430 | let auth_ok = _IncomingAuthenticationOkTestMessage.bytes() 431 | _tcp_connection.send(auth_ok) 432 | end 433 | -------------------------------------------------------------------------------- /postgres/_test_query.pony: -------------------------------------------------------------------------------- 1 | use lori = "lori" 2 | use "pony_test" 3 | 4 | class \nodoc\ iso _TestQueryResults is UnitTest 5 | fun name(): String => 6 | "integration/Query/Results" 7 | 8 | fun apply(h: TestHelper) => 9 | let info = _ConnectionTestConfiguration(h.env.vars) 10 | 11 | let client = _ResultsIncludeOriginatingQueryReceiver(h) 12 | 13 | let session = Session( 14 | lori.TCPConnectAuth(h.env.root), 15 | client, 16 | info.host, 17 | info.port, 18 | info.username, 19 | info.password, 20 | info.database) 21 | 22 | h.dispose_when_done(session) 23 | h.long_test(5_000_000_000) 24 | 25 | actor \nodoc\ _ResultsIncludeOriginatingQueryReceiver is 26 | ( SessionStatusNotify 27 | & ResultReceiver ) 28 | let _h: TestHelper 29 | let _query: SimpleQuery 30 | 31 | new create(h: TestHelper) => 32 | _h = h 33 | _query = SimpleQuery("SELECT 525600::text") 34 | 35 | be pg_session_authenticated(session: Session) => 36 | session.execute(_query, this) 37 | 38 | be pg_session_authentication_failed( 39 | s: Session, 40 | reason: AuthenticationFailureReason) 41 | => 42 | _h.fail("Unable to establish connection") 43 | _h.complete(false) 44 | 45 | be pg_query_result(result: Result) => 46 | if result.query() isnt _query then 47 | _h.fail("Query in result isn't the expected query.") 48 | _h.complete(false) 49 | return 50 | end 51 | 52 | match result 53 | | let r: ResultSet => 54 | if r.rows().size() != 1 then 55 | _h.fail("Wrong number of result rows.") 56 | _h.complete(false) 57 | return 58 | end 59 | 60 | try 61 | match r.rows()(0)?.fields(0)?.value 62 | | let v: String => 63 | if v != "525600" then 64 | _h.fail("Unexpected query results.") 65 | _h.complete(false) 66 | return 67 | end 68 | else 69 | _h.fail("Unexpected query results.") 70 | _h.complete(false) 71 | return 72 | end 73 | else 74 | _h.fail("Unexpected error accessing result rows.") 75 | _h.complete(false) 76 | return 77 | end 78 | else 79 | _h.fail("Wrong result type.") 80 | _h.complete(false) 81 | return 82 | end 83 | 84 | _h.complete(true) 85 | 86 | be pg_query_failed(query: SimpleQuery, 87 | failure: (ErrorResponseMessage | ClientQueryError)) 88 | => 89 | _h.fail("Unexpected query failure") 90 | _h.complete(false) 91 | 92 | class \nodoc\ iso _TestQueryAfterAuthenticationFailure is UnitTest 93 | """ 94 | Test querying after an authetication failure. 95 | """ 96 | fun name(): String => 97 | "integration/Query/AfterAuthenticationFailure" 98 | 99 | fun apply(h: TestHelper) => 100 | let info = _ConnectionTestConfiguration(h.env.vars) 101 | 102 | let session = Session( 103 | lori.TCPConnectAuth(h.env.root), 104 | _QueryAfterAuthenticationFailureNotify(h), 105 | info.host, 106 | info.port, 107 | info.username, 108 | info.password + " " + info.password, 109 | info.database) 110 | 111 | h.dispose_when_done(session) 112 | h.long_test(5_000_000_000) 113 | 114 | actor \nodoc\ _QueryAfterAuthenticationFailureNotify is 115 | ( SessionStatusNotify 116 | & ResultReceiver ) 117 | let _h: TestHelper 118 | let _query: SimpleQuery 119 | 120 | new create(h: TestHelper) => 121 | _h = h 122 | _query = SimpleQuery("select * from free_candy") 123 | 124 | be pg_session_authenticated(session: Session) => 125 | _h.fail("Unexpected successful authentication") 126 | _h.complete(false) 127 | 128 | be pg_session_authentication_failed( 129 | session: Session, 130 | reason: AuthenticationFailureReason) 131 | => 132 | session.execute(_query, this) 133 | 134 | be pg_query_result(result: Result) => 135 | _h.fail("Unexpected query result received") 136 | _h.complete(false) 137 | 138 | be pg_query_failed(query: SimpleQuery, 139 | failure: (ErrorResponseMessage | ClientQueryError)) 140 | => 141 | if (query is _query) and (failure is SessionClosed) then 142 | _h.complete(true) 143 | else 144 | _h.complete(false) 145 | end 146 | 147 | class \nodoc\ iso _TestQueryAfterConnectionFailure is UnitTest 148 | """ 149 | Test to verify that querying after connection failures are handled correctly. 150 | Currently, we set up a bad connect attempt by taking the valid port number that would allow a connect and reversing it to create an attempt to connect on a port that nothing should be listening on. 151 | """ 152 | fun name(): String => 153 | "integration/Query/AfterConnectionFailure" 154 | 155 | fun apply(h: TestHelper) => 156 | let info = _ConnectionTestConfiguration(h.env.vars) 157 | 158 | let session = Session( 159 | lori.TCPConnectAuth(h.env.root), 160 | _QueryAfterConnectionFailureNotify(h), 161 | info.host, 162 | info.port.reverse(), 163 | info.username, 164 | info.password, 165 | info.database) 166 | 167 | h.dispose_when_done(session) 168 | h.long_test(5_000_000_000) 169 | 170 | actor \nodoc\ _QueryAfterConnectionFailureNotify is 171 | ( SessionStatusNotify 172 | & ResultReceiver ) 173 | let _h: TestHelper 174 | let _query: SimpleQuery 175 | 176 | new create(h: TestHelper) => 177 | _h = h 178 | _query = SimpleQuery("select * from free_candy") 179 | 180 | be pg_session_connected(session: Session) => 181 | _h.fail("Unexpected successful connection") 182 | _h.complete(false) 183 | 184 | be pg_session_connection_failed(session: Session) => 185 | session.execute(_query, this) 186 | 187 | be pg_query_result(result: Result) => 188 | _h.fail("Unexpected query result received") 189 | _h.complete(false) 190 | 191 | be pg_query_failed(query: SimpleQuery, 192 | failure: (ErrorResponseMessage | ClientQueryError)) 193 | => 194 | if (query is _query) and (failure is SessionClosed) then 195 | _h.complete(true) 196 | else 197 | _h.complete(false) 198 | end 199 | 200 | class \nodoc\ iso _TestQueryAfterSessionHasBeenClosed is UnitTest 201 | """ 202 | Test querying after we've closed the session. 203 | """ 204 | fun name(): String => 205 | "integration/Query/AfterSessionHasBeenClosed" 206 | 207 | fun apply(h: TestHelper) => 208 | let info = _ConnectionTestConfiguration(h.env.vars) 209 | 210 | let session = Session( 211 | lori.TCPConnectAuth(h.env.root), 212 | _QueryAfterSessionHasBeenClosedNotify(h), 213 | info.host, 214 | info.port, 215 | info.username, 216 | info.password, 217 | info.database) 218 | 219 | h.dispose_when_done(session) 220 | h.long_test(5_000_000_000) 221 | 222 | actor \nodoc\ _QueryAfterSessionHasBeenClosedNotify is 223 | ( SessionStatusNotify 224 | & ResultReceiver ) 225 | let _h: TestHelper 226 | let _query: SimpleQuery 227 | 228 | new create(h: TestHelper) => 229 | _h = h 230 | _query = SimpleQuery("select * from free_candy") 231 | 232 | be pg_session_authenticated(session: Session) => 233 | session.close() 234 | 235 | be pg_session_authentication_failed( 236 | session: Session, 237 | reason: AuthenticationFailureReason) 238 | => 239 | _h.fail("Unexpected authentication failure") 240 | 241 | be pg_session_shutdown(session: Session) => 242 | session.execute(_query, this) 243 | 244 | be pg_query_result(result: Result) => 245 | _h.fail("Unexpected query result received") 246 | _h.complete(false) 247 | 248 | be pg_query_failed(query: SimpleQuery, 249 | failure: (ErrorResponseMessage | ClientQueryError)) 250 | => 251 | if (query is _query) and (failure is SessionClosed) then 252 | _h.complete(true) 253 | else 254 | _h.complete(false) 255 | end 256 | 257 | class \nodoc\ iso _TestQueryOfNonExistentTable is UnitTest 258 | fun name(): String => 259 | "integration/Query/OfNonExistentTable" 260 | 261 | fun apply(h: TestHelper) => 262 | let info = _ConnectionTestConfiguration(h.env.vars) 263 | 264 | let client = _NonExistentTableQueryReceiver(h) 265 | 266 | let session = Session( 267 | lori.TCPConnectAuth(h.env.root), 268 | client, 269 | info.host, 270 | info.port, 271 | info.username, 272 | info.password, 273 | info.database) 274 | 275 | h.dispose_when_done(session) 276 | h.long_test(5_000_000_000) 277 | 278 | actor \nodoc\ _NonExistentTableQueryReceiver is 279 | ( SessionStatusNotify 280 | & ResultReceiver ) 281 | let _h: TestHelper 282 | let _query: SimpleQuery 283 | 284 | new create(h: TestHelper) => 285 | _h = h 286 | _query = SimpleQuery("SELECT * from THIS_TABLE_DOESNT_EXIST") 287 | 288 | be pg_session_authenticated(session: Session) => 289 | session.execute(_query, this) 290 | 291 | be pg_session_authentication_failed( 292 | s: Session, 293 | reason: AuthenticationFailureReason) 294 | => 295 | _h.fail("Unable to establish connection") 296 | _h.complete(false) 297 | 298 | be pg_query_result(result: Result) => 299 | _h.fail("Query unexpectedly succeeded.") 300 | _h.complete(false) 301 | 302 | be pg_query_failed(query: SimpleQuery, 303 | failure: (ErrorResponseMessage | ClientQueryError)) 304 | => 305 | // TODO enhance this by checking the failure 306 | if query is _query then 307 | _h.complete(true) 308 | else 309 | _h.fail("Incorrect query paramter received.") 310 | _h.complete(false) 311 | end 312 | 313 | class \nodoc\ _TestCreateAndDropTable is UnitTest 314 | """ 315 | Tests expectations around client API for creating a table and dropping a 316 | table. 317 | """ 318 | fun name(): String => 319 | "integration/Query/CreateAndDropTable" 320 | 321 | fun apply(h: TestHelper) => 322 | let info = _ConnectionTestConfiguration(h.env.vars) 323 | 324 | let queries = recover iso 325 | Array[SimpleQuery] 326 | .>push( 327 | SimpleQuery( 328 | """ 329 | CREATE TABLE CreateAndDropTable ( 330 | fu VARCHAR(50) NOT NULL, 331 | bar VARCHAR(50) NOT NULL 332 | ) 333 | """)) 334 | .>push(SimpleQuery("DROP TABLE CreateAndDropTable")) 335 | end 336 | 337 | let client = _AllSuccessQueryRunningClient(h, info, consume queries) 338 | 339 | h.dispose_when_done(client) 340 | h.long_test(5_000_000_000) 341 | 342 | class \nodoc\ _TestInsertAndDelete is UnitTest 343 | """ 344 | Tests expectations around client API for creating inserting records into a 345 | table and then deleting them. 346 | """ 347 | fun name(): String => 348 | "integration/Query/InsertAndDelete" 349 | 350 | fun apply(h: TestHelper) => 351 | let info = _ConnectionTestConfiguration(h.env.vars) 352 | 353 | let queries = recover iso 354 | Array[SimpleQuery] 355 | .>push( 356 | SimpleQuery( 357 | """ 358 | CREATE TABLE i_and_d ( 359 | fu VARCHAR(50) NOT NULL, 360 | bar VARCHAR(50) NOT NULL 361 | ) 362 | """)) 363 | .>push(SimpleQuery( 364 | "INSERT INTO i_and_d (fu, bar) VALUES ('fu', 'bar')")) 365 | .>push(SimpleQuery( 366 | "INSERT INTO i_and_d (fu, bar) VALUES('pony', 'lang')")) 367 | .>push(SimpleQuery("DELETE FROM i_and_d")) 368 | .>push(SimpleQuery("DROP TABLE i_and_d")) 369 | end 370 | 371 | let client = _AllSuccessQueryRunningClient(h, info, consume queries) 372 | 373 | h.dispose_when_done(client) 374 | h.long_test(5_000_000_000) 375 | 376 | actor \nodoc\ _AllSuccessQueryRunningClient is 377 | ( SessionStatusNotify 378 | & ResultReceiver ) 379 | let _h: TestHelper 380 | let _queries: Array[SimpleQuery] 381 | let _session: Session 382 | 383 | new create(h: TestHelper, 384 | info: _ConnectionTestConfiguration, 385 | queries: Array[SimpleQuery] iso) 386 | => 387 | _h = h 388 | _queries = consume queries 389 | 390 | _session = Session( 391 | lori.TCPConnectAuth(h.env.root), 392 | this, 393 | info.host, 394 | info.port, 395 | info.username, 396 | info.password, 397 | info.database) 398 | 399 | be pg_session_authenticated(session: Session) => 400 | try 401 | let q = _queries(0)? 402 | session.execute(q, this) 403 | else 404 | _h.fail("Unexpected failure trying to run first query.") 405 | _h.complete(false) 406 | end 407 | 408 | be pg_session_authentication_failed( 409 | s: Session, 410 | reason: AuthenticationFailureReason) 411 | => 412 | _h.fail("Unable to establish connection") 413 | _h.complete(false) 414 | 415 | be pg_query_result(result: Result) => 416 | try 417 | let q = _queries.shift()? 418 | if result.query() is q then 419 | if _queries.size() > 0 then 420 | _session.execute(_queries(0)?, this) 421 | else 422 | _h.complete(true) 423 | end 424 | end 425 | else 426 | _h.fail("Unexpected failure to validate query results.") 427 | _h.complete(false) 428 | end 429 | 430 | be pg_query_failed(query: SimpleQuery, 431 | failure: (ErrorResponseMessage | ClientQueryError)) 432 | => 433 | _h.fail("Unexpected for query: " + query.string) 434 | _h.complete(false) 435 | 436 | be dispose() => 437 | _session.close() 438 | 439 | class \nodoc\ iso _TestEmptyQuery is UnitTest 440 | fun name(): String => 441 | "integration/Query/EmptyQuery" 442 | 443 | fun apply(h: TestHelper) => 444 | let info = _ConnectionTestConfiguration(h.env.vars) 445 | 446 | let client = _EmptyQueryReceiver(h) 447 | 448 | let session = Session( 449 | lori.TCPConnectAuth(h.env.root), 450 | client, 451 | info.host, 452 | info.port, 453 | info.username, 454 | info.password, 455 | info.database) 456 | 457 | h.dispose_when_done(session) 458 | h.long_test(5_000_000_000) 459 | 460 | actor \nodoc\ _EmptyQueryReceiver is 461 | ( SessionStatusNotify 462 | & ResultReceiver ) 463 | let _h: TestHelper 464 | let _query: SimpleQuery 465 | 466 | new create(h: TestHelper) => 467 | _h = h 468 | _query = SimpleQuery("") 469 | 470 | be pg_session_authenticated(session: Session) => 471 | session.execute(_query, this) 472 | 473 | be pg_session_authentication_failed( 474 | s: Session, 475 | reason: AuthenticationFailureReason) 476 | => 477 | _h.fail("Unable to establish connection") 478 | _h.complete(false) 479 | 480 | be pg_query_result(result: Result) => 481 | if result.query() isnt _query then 482 | _h.fail("Query in result isn't the expected query.") 483 | _h.complete(false) 484 | return 485 | end 486 | 487 | _h.complete(true) 488 | 489 | be pg_query_failed(query: SimpleQuery, 490 | failure: (ErrorResponseMessage | ClientQueryError)) 491 | => 492 | _h.fail("Unexpected query failure") 493 | _h.complete(false) 494 | -------------------------------------------------------------------------------- /postgres/session.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use lori = "lori" 3 | 4 | actor Session is (lori.TCPConnectionActor & lori.ClientLifecycleEventReceiver) 5 | var state: _SessionState 6 | var _tcp_connection: lori.TCPConnection = lori.TCPConnection.none() 7 | 8 | new create( 9 | auth': lori.TCPConnectAuth, 10 | notify': SessionStatusNotify, 11 | host': String, 12 | service': String, 13 | user': String, 14 | password': String, 15 | database': String) 16 | => 17 | state = _SessionUnopened(notify', user', password', database') 18 | 19 | _tcp_connection = lori.TCPConnection.client(auth', 20 | host', 21 | service', 22 | "", 23 | this, 24 | this) 25 | 26 | be execute(query: SimpleQuery, receiver: ResultReceiver) => 27 | """ 28 | Execute a query. 29 | """ 30 | state.execute(this, query, receiver) 31 | 32 | be close() => 33 | """ 34 | Hard closes the connection. Terminates as soon as possible without waiting 35 | for outstanding queries to finish. 36 | """ 37 | state.close(this) 38 | 39 | be _process_again() => 40 | state.process_responses(this) 41 | 42 | fun ref _on_connected() => 43 | state.on_connected(this) 44 | 45 | fun ref _on_connection_failure() => 46 | state.on_failure(this) 47 | 48 | fun ref _on_received(data: Array[U8] iso) => 49 | state.on_received(this, consume data) 50 | 51 | fun ref _connection(): lori.TCPConnection => 52 | _tcp_connection 53 | 54 | fun ref _next_lifecycle_event_receiver(): None => 55 | None 56 | 57 | // Possible session states 58 | class ref _SessionUnopened is _ConnectableState 59 | let _notify: SessionStatusNotify 60 | let _user: String 61 | let _password: String 62 | let _database: String 63 | 64 | new ref create(notify': SessionStatusNotify, 65 | user': String, 66 | password': String, 67 | database': String) 68 | => 69 | _notify = notify' 70 | _user = user' 71 | _password = password' 72 | _database = database' 73 | 74 | fun ref execute(s: Session ref, q: SimpleQuery, r: ResultReceiver) => 75 | r.pg_query_failed(q, SesssionNeverOpened) 76 | 77 | fun user(): String => 78 | _user 79 | 80 | fun password(): String => 81 | _password 82 | 83 | fun database(): String => 84 | _database 85 | 86 | fun notify(): SessionStatusNotify => 87 | _notify 88 | 89 | class ref _SessionClosed is (_NotConnectableState & _UnconnectedState) 90 | fun ref execute(s: Session ref, q: SimpleQuery, r: ResultReceiver) => 91 | r.pg_query_failed(q, SessionClosed) 92 | 93 | class ref _SessionConnected is _AuthenticableState 94 | let _notify: SessionStatusNotify 95 | let _user: String 96 | let _password: String 97 | let _database: String 98 | let _readbuf: Reader = _readbuf.create() 99 | 100 | new ref create(notify': SessionStatusNotify, 101 | user': String, 102 | password': String, 103 | database': String) 104 | => 105 | _notify = notify' 106 | _user = user' 107 | _password = password' 108 | _database = database' 109 | 110 | fun ref execute(s: Session ref, q: SimpleQuery, r: ResultReceiver) => 111 | r.pg_query_failed(q, SessionNotAuthenticated) 112 | 113 | fun ref on_shutdown(s: Session ref) => 114 | _readbuf.clear() 115 | 116 | fun user(): String => 117 | _user 118 | 119 | fun password(): String => 120 | _password 121 | 122 | fun ref readbuf(): Reader => 123 | _readbuf 124 | 125 | fun notify(): SessionStatusNotify => 126 | _notify 127 | 128 | // TODO SEAN 129 | // some of these callbacks have if statements for if we are "in query", should 130 | // add an additional level of state machine for query state of "in flight" or 131 | // "no query in flight" 132 | // Also need to handle things like "row data" arrives after "command complete" 133 | // etc where message ordering is violated in an unexpected way. 134 | class _SessionLoggedIn is _AuthenticatedState 135 | var _queryable: Bool = false 136 | var _query_in_flight: Bool = false 137 | let _query_queue: Array[(SimpleQuery, ResultReceiver)] = _query_queue.create() 138 | let _notify: SessionStatusNotify 139 | let _readbuf: Reader 140 | var _data_rows: Array[Array[(String|None)] val] iso 141 | var _row_description: Array[(String, U32)] val 142 | 143 | new ref create(notify': SessionStatusNotify, readbuf': Reader) => 144 | _notify = notify' 145 | _readbuf = readbuf' 146 | _data_rows = recover iso Array[Array[(String|None)] val] end 147 | _row_description = recover val Array[(String, U32)] end 148 | 149 | fun ref on_ready_for_query(s: Session ref, msg: _ReadyForQueryMessage) => 150 | if msg.idle() then 151 | if _query_in_flight then 152 | // If there was a query in flight, we are now done with it. 153 | try 154 | _query_queue.shift()? 155 | end 156 | _query_in_flight = false 157 | end 158 | _queryable = true 159 | _run_query(s) 160 | else 161 | _queryable = false 162 | end 163 | 164 | fun ref on_command_complete(s: Session ref, msg: _CommandCompleteMessage) => 165 | """ 166 | A command has completed, that might mean the active is query is done. At 167 | this point we don't know. We grab the active query from the head of the 168 | query queue while leaving it in place and inform the receiver of a success 169 | for at least one part of the query. 170 | """ 171 | if _query_in_flight then 172 | try 173 | (let query, let receiver) = _query_queue(0)? 174 | let rows = _data_rows = recover iso 175 | Array[Array[(String|None)] val].create() 176 | end 177 | 178 | try 179 | // TODO SEAN 180 | // there are a number of possibilities here. 181 | // we have row description but not rows. that's an error. 182 | // we have rows but no row description. that's an error. 183 | // we have rows and row description but the command isn't a "SELECT" 184 | // or similar command. that's an error. 185 | // we have a select with proper data 186 | // we have a command that has no rows and the id contains number of 187 | // rows impacted 188 | // we have a command that has no rows and the id doesn't contain the 189 | // number of rows impacted 190 | if rows.size() > 0 then 191 | let rows_object = _RowsBuilder(consume rows, _row_description)? 192 | receiver.pg_query_result(ResultSet(query, rows_object, msg.id)) 193 | else 194 | receiver.pg_query_result(RowModifying(query, msg.id, msg.value)) 195 | end 196 | else 197 | receiver.pg_query_failed(query, DataError) 198 | end 199 | else 200 | _Unreachable() 201 | end 202 | else 203 | // This should never happen. If it does, something has gone horribly 204 | // and we need to shutdown. 205 | shutdown(s) 206 | end 207 | 208 | fun ref on_empty_query_response(s: Session ref) => 209 | if _query_in_flight then 210 | // TODO SEAN 211 | // we should never have any rows or row description, that's an error 212 | // if we do. 213 | try 214 | (let query, let receiver) = _query_queue(0)? 215 | receiver.pg_query_result(SimpleResult(query)) 216 | else 217 | _Unreachable() 218 | end 219 | else 220 | // This should never happen. If it does, something has gone horribly 221 | // and we need to shutdown. 222 | shutdown(s) 223 | end 224 | 225 | fun ref on_error_response(s: Session ref, msg: ErrorResponseMessage) => 226 | if _query_in_flight then 227 | try 228 | (let query, let receiver) = _query_queue(0)? 229 | receiver.pg_query_failed(query, msg) 230 | else 231 | _Unreachable() 232 | end 233 | else 234 | // This should never happen. If it does, something has gone horribly 235 | // and we need to shutdown. 236 | shutdown(s) 237 | end 238 | 239 | fun ref on_data_row(s: Session ref, msg: _DataRowMessage) => 240 | if _query_in_flight then 241 | _data_rows.push(msg.columns) 242 | else 243 | // This should never happen. If it does, something has gone horribly 244 | // and we need to shutdown. 245 | shutdown(s) 246 | end 247 | 248 | fun ref execute(s: Session ref, 249 | query: SimpleQuery, 250 | receiver: ResultReceiver) 251 | => 252 | _query_queue.push((query, receiver)) 253 | _run_query(s) 254 | 255 | fun ref on_row_description(s: Session ref, msg: _RowDescriptionMessage) => 256 | // TODO we should verify that only get 1 of these per in flight query 257 | if _query_in_flight then 258 | _row_description = msg.columns 259 | else 260 | // This should never happen. If it does, something has gone horribly 261 | // and we need to shutdown. 262 | shutdown(s) 263 | end 264 | 265 | fun ref _run_query(s: Session ref) => 266 | try 267 | if _queryable and (_query_queue.size() > 0) then 268 | (let query, _) = _query_queue(0)? 269 | _queryable = false 270 | _query_in_flight = true 271 | let msg = _FrontendMessage.query(query.string) 272 | s._connection().send(msg) 273 | end 274 | else 275 | _Unreachable() 276 | end 277 | 278 | fun ref on_shutdown(s: Session ref) => 279 | _readbuf.clear() 280 | for queue_item in _query_queue.values() do 281 | (let query, let receiver) = queue_item 282 | receiver.pg_query_failed(query, SessionClosed) 283 | end 284 | _query_queue.clear() 285 | 286 | fun ref readbuf(): Reader => 287 | _readbuf 288 | 289 | fun notify(): SessionStatusNotify => 290 | _notify 291 | 292 | interface _SessionState 293 | fun on_connected(s: Session ref) 294 | """ 295 | Called when a connection is established with the server. 296 | """ 297 | fun on_failure(s: Session ref) 298 | """ 299 | Called if we fail to establish a connection with the server. 300 | """ 301 | fun ref on_authentication_ok(s: Session ref) 302 | """ 303 | Called when we successfully authenticate with the server. 304 | """ 305 | fun ref on_authentication_failed( 306 | s: Session ref, 307 | reason: AuthenticationFailureReason) 308 | """ 309 | Called if we failed to successfully authenticate with the server. 310 | """ 311 | fun on_authentication_md5_password(s: Session ref, 312 | msg: _AuthenticationMD5PasswordMessage) 313 | """ 314 | Called if the server requests we autheticate using the Postgres MD5 315 | password scheme. 316 | """ 317 | fun ref close(s: Session ref) 318 | """ 319 | The client received a message to close. Unlike `shutdown`, this should never 320 | be an illegal state as we can receive messages to take actions from outside 321 | at any point. If received when "illegal", it should be silently ignored. If 322 | received when "legal", then `shutdown` should be called. 323 | """ 324 | fun ref shutdown(s: Session ref) 325 | """ 326 | Called when we are shutting down the session. 327 | """ 328 | fun ref on_received(s: Session ref, data: Array[U8] iso) 329 | """ 330 | Called when we receive data from the server. 331 | """ 332 | fun ref execute(s: Session ref, query: SimpleQuery, receiver: ResultReceiver) 333 | """ 334 | Called when a client requests a query execution. 335 | """ 336 | fun ref on_ready_for_query(s: Session ref, msg: _ReadyForQueryMessage) 337 | """ 338 | Called when the server sends a "ready for query" message 339 | """ 340 | fun ref process_responses(s: Session ref) 341 | """ 342 | Called to process responses we've received from the server after the data 343 | has been parsed into messages. 344 | """ 345 | fun ref on_command_complete(s: Session ref, msg: _CommandCompleteMessage) 346 | """ 347 | Called when the server has completed running an individual command. If a 348 | query was a single command, this will be followed by "ready for query". If 349 | the query contained multiple commands then the results of additional 350 | commands should be expected. Generally, the arrival of "command complete" is 351 | when we would want to notify the client of the results or subset of results 352 | available so far for the active query. 353 | 354 | Queries that resulted in a error will not have "command complete" sent. 355 | """ 356 | fun ref on_empty_query_response(s: Session ref) 357 | """ 358 | Called when the server has completed running an individual command that was 359 | an empty query. This is effectively "command complete" but for the special 360 | case of "empty query". 361 | """ 362 | fun ref on_error_response(s: Session ref, msg: ErrorResponseMessage) 363 | """ 364 | Called when the server has encountered an error. Not all errors are called 365 | using this callback. For example, we intercept authorization errors and 366 | handle them using a specialized callback. All errors without a specialized 367 | callback are handled by `on_error_response`. 368 | """ 369 | 370 | fun ref on_data_row(s: Session ref, msg: _DataRowMessage) 371 | """ 372 | Called when a data row is received from the server. 373 | """ 374 | 375 | fun ref on_row_description(s: Session ref, msg: _RowDescriptionMessage) 376 | """ 377 | Called when a row description is receivedfrom the server. 378 | """ 379 | 380 | trait _ConnectableState is _UnconnectedState 381 | """ 382 | An unopened session that can be connected to a server. 383 | """ 384 | fun on_connected(s: Session ref) => 385 | s.state = _SessionConnected(notify(), user(), password(), database()) 386 | notify().pg_session_connected(s) 387 | _send_startup_message(s) 388 | 389 | fun on_failure(s: Session ref) => 390 | s.state = _SessionClosed 391 | notify().pg_session_connection_failed(s) 392 | 393 | fun _send_startup_message(s: Session ref) => 394 | let msg = _FrontendMessage.startup(user(), database()) 395 | s._connection().send(msg) 396 | 397 | fun user(): String 398 | fun password(): String 399 | fun database(): String 400 | fun notify(): SessionStatusNotify 401 | 402 | trait _NotConnectableState 403 | """ 404 | A session that if it gets messages related to connect to a server, then 405 | something has gone wrong with the state machine. 406 | """ 407 | fun on_connected(s: Session ref) => 408 | _IllegalState() 409 | 410 | fun on_failure(s: Session ref) => 411 | _IllegalState() 412 | 413 | trait _ConnectedState is _NotConnectableState 414 | """ 415 | A connected session. Connected sessions are not connectable as they have 416 | already been connected. 417 | """ 418 | fun ref on_received(s: Session ref, data: Array[U8] iso) => 419 | readbuf().append(consume data) 420 | process_responses(s) 421 | 422 | fun ref process_responses(s: Session ref) => 423 | _ResponseMessageParser(s, readbuf()) 424 | 425 | fun ref close(s: Session ref) => 426 | shutdown(s) 427 | 428 | fun ref shutdown(s: Session ref) => 429 | on_shutdown(s) 430 | s._connection().close() 431 | notify().pg_session_shutdown(s) 432 | s.state = _SessionClosed 433 | 434 | fun ref on_shutdown(s: Session ref) => 435 | """ 436 | Called on implementers to allow them to clear state when shutting down. 437 | """ 438 | 439 | fun ref readbuf(): Reader 440 | 441 | fun notify(): SessionStatusNotify 442 | 443 | trait _UnconnectedState is (_NotAuthenticableState & _NotAuthenticated) 444 | """ 445 | A session that isn't connected. Either because it was never opened or because 446 | it has been closed. Unconnected sessions are not eligible to be authenticated 447 | and receiving an authentication event while unconnected is an error. 448 | """ 449 | fun ref on_received(s: Session ref, data: Array[U8] iso) => 450 | // It is possible we will continue to receive data after we have closed 451 | // so this isn't an invalid state. We should silently drop the data. If 452 | // "not yet opened" and "closed" were different states, rather than a single 453 | // "unconnected" then we would want to call illegal state if `on_received` 454 | // was called when the state was "not yet opened". 455 | None 456 | 457 | fun ref process_responses(s: Session ref) => 458 | None 459 | 460 | fun ref close(s: Session ref) => 461 | None 462 | 463 | fun ref shutdown(s: Session ref) => 464 | ifdef debug then 465 | _IllegalState() 466 | end 467 | 468 | trait _AuthenticableState is (_ConnectedState & _NotAuthenticated) 469 | """ 470 | A session that can be authenticated. All authenticatible sessions are 471 | connected sessions, but not all connected sessions are autheticable. Once a 472 | session has been authenticated, it's an error for another authetication event 473 | to occur. 474 | """ 475 | fun ref on_authentication_ok(s: Session ref) => 476 | s.state = _SessionLoggedIn(notify(), readbuf()) 477 | notify().pg_session_authenticated(s) 478 | 479 | fun ref on_authentication_failed(s: Session ref, r: AuthenticationFailureReason) => 480 | notify().pg_session_authentication_failed(s, r) 481 | shutdown(s) 482 | 483 | fun on_authentication_md5_password(s: Session ref, 484 | msg: _AuthenticationMD5PasswordMessage) 485 | => 486 | let md5_password = _MD5Password(user(), password(), msg.salt) 487 | let reply = _FrontendMessage.password(md5_password) 488 | s._connection().send(reply) 489 | 490 | fun user(): String 491 | fun password(): String 492 | fun ref readbuf(): Reader 493 | fun notify(): SessionStatusNotify 494 | 495 | trait _NotAuthenticableState 496 | """ 497 | A session that isn't eligible to be authenticated. Only connected sessions 498 | that haven't yet been authenticated are eligible to be authenticated. 499 | """ 500 | fun ref on_authentication_ok(s: Session ref) => 501 | _IllegalState() 502 | 503 | fun ref on_authentication_failed( 504 | s: Session ref, 505 | r: AuthenticationFailureReason) 506 | => 507 | _IllegalState() 508 | 509 | fun on_authentication_md5_password(s: Session ref, 510 | msg: _AuthenticationMD5PasswordMessage) 511 | => 512 | _IllegalState() 513 | 514 | trait _AuthenticatedState is (_ConnectedState & _NotAuthenticableState) 515 | """ 516 | A connected and authenticated session. Connected sessions are not connectable 517 | as they have already been connected. Authenticated sessions are not 518 | authenticable as they have already been authenticated. 519 | """ 520 | 521 | 522 | trait _NotAuthenticated 523 | """ 524 | A session that has yet to be authenticated. Before being authenticated, then 525 | all "query related" commands should not be received. 526 | """ 527 | fun ref on_command_complete(s: Session ref, msg: _CommandCompleteMessage) => 528 | _IllegalState() 529 | 530 | fun ref on_data_row(s: Session ref, msg: _DataRowMessage) => 531 | _IllegalState() 532 | 533 | fun ref on_empty_query_response(s: Session ref) => 534 | _IllegalState() 535 | 536 | fun ref on_error_response(s: Session ref, msg: ErrorResponseMessage) => 537 | _IllegalState() 538 | 539 | fun ref on_ready_for_query(s: Session ref, msg: _ReadyForQueryMessage) => 540 | _IllegalState() 541 | 542 | fun ref on_row_description(s: Session ref, msg: _RowDescriptionMessage) => 543 | _IllegalState() 544 | -------------------------------------------------------------------------------- /postgres/_test_response_parser.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use "collections" 3 | use "pony_test" 4 | use "random" 5 | 6 | // TODO SEAN 7 | // we need tests that verify a chain of messages and that we get the expected 8 | // message type. we could validate the contents as well, but i think for a start 9 | // just validated that we got A, B, C, C, C, D would be good. 10 | // This would provide protection against not reading full messages correctly 11 | // which is currently not covered. For example not handling the null terminator 12 | // from command complete would pass tests herein but would cause the next 13 | // message to incorrectly parse. That isn't currently covered. 14 | class \nodoc\ iso _TestResponseParserEmptyBuffer is UnitTest 15 | """ 16 | Verify that handling an empty buffer to the parser returns `None` 17 | """ 18 | fun name(): String => 19 | "ResponseParser/EmptyBuffer" 20 | 21 | fun apply(h: TestHelper) ? => 22 | let empty: Reader = Reader 23 | 24 | if _ResponseParser(empty)? isnt None then 25 | h.fail() 26 | end 27 | 28 | class \nodoc\ iso _TestResponseParserIncompleteMessage is UnitTest 29 | """ 30 | Verify that handing a buffer that isn't a complete message to the parser 31 | returns `None` 32 | """ 33 | fun name(): String => 34 | "ResponseParser/IncompleteMessage" 35 | 36 | fun apply(h: TestHelper) ? => 37 | let bytes = _IncomingAuthenticationOkTestMessage.bytes() 38 | let complete_message_index = bytes.size() 39 | 40 | for i in Range(0, complete_message_index) do 41 | let r: Reader = Reader 42 | let s: Array[U8] val = bytes.trim(0, i) 43 | r.append(s) 44 | 45 | if _ResponseParser(r)? isnt None then 46 | h.fail( 47 | "Parsing incomplete message with size of " + 48 | i.string() + 49 | " didn't return None.") 50 | end 51 | end 52 | 53 | class \nodoc\ iso _TestResponseParserJunkMessage is UnitTest 54 | """ 55 | Verify that handing a buffer contains "junk" data leads to an error. 56 | """ 57 | fun name(): String => 58 | "ResponseParser/JunkMessage" 59 | 60 | fun apply(h: TestHelper) => 61 | h.assert_error({() ? => 62 | let bytes = _IncomingJunkTestMessage.bytes() 63 | let r: Reader = Reader 64 | r.append(bytes) 65 | _ResponseParser(r)? }) 66 | 67 | class \nodoc\ iso _TestResponseParserAuthenticationOkMessage is UnitTest 68 | """ 69 | Verify that AuthenticationOk messages are parsed correctly 70 | """ 71 | fun name(): String => 72 | "ResponseParser/AuthenticationOkMessage" 73 | 74 | fun apply(h: TestHelper) ? => 75 | let bytes = _IncomingAuthenticationOkTestMessage.bytes() 76 | let r: Reader = Reader 77 | r.append(bytes) 78 | 79 | if _ResponseParser(r)? isnt _AuthenticationOkMessage then 80 | h.fail() 81 | end 82 | 83 | class \nodoc\ iso _TestResponseParserAuthenticationMD5PasswordMessage is UnitTest 84 | """ 85 | Verify that AuthenticationMD5Password messages are parsed correctly 86 | """ 87 | fun name(): String => 88 | "ResponseParser/AuthenticationMD5PasswordMessage" 89 | 90 | fun apply(h: TestHelper) ? => 91 | let salt = "7669" 92 | let bytes = _IncomingAuthenticationMD5PasswordTestMessage(salt).bytes() 93 | let r: Reader = Reader 94 | r.append(bytes) 95 | 96 | match _ResponseParser(r)? 97 | | let m: _AuthenticationMD5PasswordMessage => 98 | if m.salt != salt then 99 | h.fail("Salt not correctly parsed.") 100 | end 101 | else 102 | h.fail("Wrong message returned.") 103 | end 104 | 105 | class \nodoc\ iso _TestResponseParserErrorResponseMessage is UnitTest 106 | """ 107 | Verify that ErrorResponse messages are parsed correctly 108 | """ 109 | fun name(): String => 110 | "ResponseParser/ErrorResponseMessage" 111 | 112 | fun apply(h: TestHelper) ? => 113 | let severity = "ERROR" 114 | let code = "7669" 115 | let message = "Who's gonna die when the old database dies?" 116 | let bytes = 117 | _IncomingErrorResponseTestMessage(severity, code, message).bytes() 118 | let r: Reader = Reader 119 | r.append(bytes) 120 | 121 | match _ResponseParser(r)? 122 | | let m: ErrorResponseMessage => 123 | if m.severity != severity then 124 | h.fail("Severity not correctly parsed.") 125 | end 126 | if m.code != code then 127 | h.fail("Code not correctly parsed.") 128 | end 129 | if m.message != message then 130 | h.fail("Message not correctly parsed.") 131 | end 132 | else 133 | h.fail("Wrong message returned.") 134 | end 135 | 136 | class \nodoc\ iso _TestResponseParserCommandCompleteMessage is UnitTest 137 | """ 138 | Verifies expected handling of various command complete messages. 139 | """ 140 | fun name(): String => 141 | "ResponseParser/CommandCompleteMessage" 142 | 143 | fun apply(h: TestHelper) ? => 144 | _test_expected(h, "INSERT 1 5", "INSERT", 5)? 145 | _test_expected(h, "DELETE 18", "DELETE", 18)? 146 | _test_expected(h, "UPDATE 2047", "UPDATE", 2047)? 147 | _test_expected(h, "SELECT 5012", "SELECT", 5012)? 148 | _test_expected(h, "MOVE 11", "MOVE", 11)? 149 | _test_expected(h, "FETCH 7", "FETCH", 7)? 150 | _test_expected(h, "COPY 7", "COPY", 7)? 151 | _test_expected(h, "CREATE TABLE", "CREATE TABLE", 0)? 152 | _test_expected(h, "DROP TABLE", "DROP TABLE", 0)? 153 | _test_expected(h, "FUTURE PROOF", "FUTURE PROOF", 0)? 154 | _test_expected(h, "FUTURE PROOF 2", "FUTURE PROOF", 2)? 155 | 156 | _test_error(h, "") 157 | 158 | fun _test_expected(h: TestHelper, i: String, id: String, value: USize) ? => 159 | let bytes = _IncomingCommandCompleteTestMessage(i).bytes() 160 | let r: Reader = Reader.>append(bytes) 161 | 162 | match _ResponseParser(r)? 163 | | let m: _CommandCompleteMessage => 164 | h.assert_eq[String](m.id, id) 165 | h.assert_eq[USize](m.value, value) 166 | else 167 | h.fail("Wrong message returned.") 168 | end 169 | 170 | fun _test_error(h: TestHelper, i: String) => 171 | h.assert_error({() ? => 172 | let bytes = _IncomingCommandCompleteTestMessage(i).bytes() 173 | let r: Reader = Reader.>append(bytes) 174 | 175 | _ResponseParser(r)? }, ("Assert error failed for " + i)) 176 | 177 | class \nodoc\ iso _TestResponseParserRowDescriptionMessage is UnitTest 178 | """ 179 | Verifies expected handling of various row description messages. 180 | """ 181 | fun name(): String => 182 | "ResponseParser/RowDescriptionMessage" 183 | 184 | fun apply(h: TestHelper) ? => 185 | let columns: Array[(String, String)] val = recover val 186 | [ ("is_it_true", "bool"); ("description", "text"); ("tiny", "int2") 187 | ("essay", "text"); ("price", "int4"); ("counter", "int8") 188 | ("money", "float4"); ("big_money", "float8") ] 189 | end 190 | let expected: Array[(String, U32)] val = recover val 191 | [ ("is_it_true", 16); ("description", 25); ("tiny", 21); ("essay", 25) 192 | ("price", 23); ("counter", 20); ("money", 700); ("big_money", 701) ] 193 | end 194 | 195 | let bytes = _IncomingRowDescriptionTestMessage(columns)?.bytes() 196 | let r: Reader = Reader.>append(bytes) 197 | 198 | match _ResponseParser(r)? 199 | | let m: _RowDescriptionMessage => 200 | h.assert_eq[USize](expected.size(), m.columns.size()) 201 | for i in Range(0, expected.size()) do 202 | h.assert_eq[String](expected(i)?._1, m.columns(i)?._1) 203 | h.assert_eq[U32](expected(i)?._2, m.columns(i)?._2) 204 | end 205 | else 206 | h.fail("Wrong message returned.") 207 | end 208 | 209 | class \nodoc\ iso _TestResponseParserMultipleMessagesAuthenticationOkFirst 210 | is UnitTest 211 | """ 212 | Verify that we correctly advance forward from an authentication ok message 213 | such that it doesn't corrupt the buffer and lead to an incorrect result for 214 | the next message. 215 | """ 216 | fun name(): String => 217 | "ResponseParser/MultipleMessages/AuthenticationOkFirst" 218 | 219 | fun apply(h: TestHelper) ? => 220 | let r: Reader = Reader 221 | r.append(_IncomingAuthenticationOkTestMessage.bytes()) 222 | r.append(_IncomingAuthenticationOkTestMessage.bytes()) 223 | 224 | if _ResponseParser(r)? isnt _AuthenticationOkMessage then 225 | h.fail("Wrong message returned for first message.") 226 | end 227 | 228 | if _ResponseParser(r)? isnt _AuthenticationOkMessage then 229 | h.fail("Wrong message returned for second message.") 230 | end 231 | 232 | class \nodoc\ iso 233 | _TestResponseParserMultipleMessagesAuthenticationMD5PasswordFirst 234 | is UnitTest 235 | """ 236 | Verify that we correctly advance forward from an authentication md5 password message such that it doesn't corrupt the buffer and lead to an incorrect 237 | result for the next message. 238 | """ 239 | fun name(): String => 240 | "ResponseParser/MultipleMessages/AuthenticationMD5PasswordFirst" 241 | 242 | fun apply(h: TestHelper) ? => 243 | let salt = "7669" 244 | let r: Reader = Reader 245 | r.append(_IncomingAuthenticationMD5PasswordTestMessage(salt).bytes()) 246 | r.append(_IncomingAuthenticationOkTestMessage.bytes()) 247 | 248 | match _ResponseParser(r)? 249 | | let m: _AuthenticationMD5PasswordMessage => 250 | if m.salt != salt then 251 | h.fail("Salt not correctly parsed.") 252 | end 253 | else 254 | h.fail("Wrong message returned for first message.") 255 | end 256 | 257 | if _ResponseParser(r)? isnt _AuthenticationOkMessage then 258 | h.fail("Wrong message returned for second message.") 259 | end 260 | 261 | class \nodoc\ iso _TestResponseParserMultipleMessagesErrorResponseFirst is UnitTest 262 | """ 263 | Verify that we correctly advance forward from an error response message such 264 | that it doesn't corrupt the buffer and lead to an incorrect result for the 265 | next message. 266 | """ 267 | fun name(): String => 268 | "ResponseParser/MultipleMessages/ErrorResponseFirst" 269 | 270 | fun apply(h: TestHelper) ? => 271 | let severity = "ERROR" 272 | let code = "7669" 273 | let message = "Who's gonna die when the old database dies?" 274 | let r: Reader = Reader 275 | r.append(_IncomingErrorResponseTestMessage(severity, code, message).bytes()) 276 | r.append(_IncomingAuthenticationOkTestMessage.bytes()) 277 | 278 | match _ResponseParser(r)? 279 | | let m: ErrorResponseMessage => 280 | if m.severity != severity then 281 | h.fail("Severity not correctly parsed.") 282 | end 283 | if m.code != code then 284 | h.fail("Code not correctly parsed.") 285 | end 286 | if m.message != message then 287 | h.fail("Message not correctly parsed.") 288 | end 289 | else 290 | h.fail("Wrong message returned for first message.") 291 | end 292 | 293 | if _ResponseParser(r)? isnt _AuthenticationOkMessage then 294 | h.fail("Wrong message returned for second message.") 295 | end 296 | 297 | class \nodoc\ iso _TestResponseParserReadyForQueryMessage is UnitTest 298 | """ 299 | Test that we parse incoming ready for query messages correctly. 300 | """ 301 | fun name(): String => 302 | "ResponseParser/ReadyForQueryMessage" 303 | 304 | fun apply(h: TestHelper) ? => 305 | _idle_test(h)? 306 | _in_transaction_block_test(h)? 307 | _failed_transaction_test(h)? 308 | _bunk(h) 309 | 310 | fun _idle_test(h: TestHelper) ? => 311 | let s: U8 = 'I' 312 | let bytes = 313 | _IncomingReadyForQueryTestMessage(s).bytes() 314 | let r: Reader = Reader 315 | r.append(bytes) 316 | 317 | match _ResponseParser(r)? 318 | | let m: _ReadyForQueryMessage => 319 | if not m.idle() then 320 | h.fail("Incorrect status.") 321 | end 322 | else 323 | h.fail("Wrong message returned.") 324 | end 325 | 326 | fun _in_transaction_block_test(h: TestHelper) ? => 327 | let s: U8 = 'T' 328 | let bytes = 329 | _IncomingReadyForQueryTestMessage(s).bytes() 330 | let r: Reader = Reader 331 | r.append(bytes) 332 | 333 | match _ResponseParser(r)? 334 | | let m: _ReadyForQueryMessage => 335 | if not m.in_transaction_block() then 336 | h.fail("Incorrect status.") 337 | end 338 | else 339 | h.fail("Wrong message returned.") 340 | end 341 | 342 | fun _failed_transaction_test(h: TestHelper) ? => 343 | let s: U8 = 'E' 344 | let bytes = 345 | _IncomingReadyForQueryTestMessage(s).bytes() 346 | let r: Reader = Reader 347 | r.append(bytes) 348 | 349 | match _ResponseParser(r)? 350 | | let m: _ReadyForQueryMessage => 351 | if not m.failed_transaction() then 352 | h.fail("Incorrect status.") 353 | end 354 | else 355 | h.fail("Wrong message returned.") 356 | end 357 | 358 | fun _bunk(h: TestHelper) => 359 | h.assert_error({() ? => 360 | let s: U8 = 'A' 361 | let bytes = 362 | _IncomingReadyForQueryTestMessage(s).bytes() 363 | let r: Reader = Reader 364 | r.append(bytes) 365 | 366 | _ResponseParser(r)? }) 367 | 368 | class \nodoc\ iso _TestResponseParserEmptyQueryResponseMessage is UnitTest 369 | """ 370 | Test that we parse incoming empty query response messages correctly. 371 | """ 372 | fun name(): String => 373 | "ResponseParser/EmptyQueryResponseMessage" 374 | 375 | fun apply(h: TestHelper) ? => 376 | let bytes = _IncomingEmptyQueryResponseTestMessage.bytes() 377 | let r: Reader = Reader.>append(bytes) 378 | 379 | match _ResponseParser(r)? 380 | | let m: _EmptyQueryResponseMessage => 381 | // All good! 382 | None 383 | else 384 | h.fail("Wrong message returned.") 385 | end 386 | 387 | class \nodoc\ iso _TestResponseParserDataRowMessage is UnitTest 388 | """ 389 | Test that we parse incoming data row messages correctly. 390 | """ 391 | fun name(): String => 392 | "ResponseParser/DataRowMessage" 393 | 394 | fun apply(h: TestHelper) ? => 395 | let columns: Array[(String | None)] val = recover val 396 | Array[(String | None)] 397 | .>push("Hello") 398 | .>push("There") 399 | .>push(None) 400 | .>push("") 401 | end 402 | 403 | let bytes = _IncomingDataRowTestMessage(columns).bytes() 404 | let r: Reader = Reader.>append(bytes) 405 | 406 | match _ResponseParser(r)? 407 | | let m: _DataRowMessage => 408 | h.assert_eq[USize](4, m.columns.size()) 409 | match m.columns(0)? 410 | | "Hello" => None 411 | else 412 | h.fail("First column not parsed correctly") 413 | end 414 | match m.columns(1)? 415 | | "There" => None 416 | else 417 | h.fail("Second column not parsed correctly") 418 | end 419 | match m.columns(2)? 420 | | None => None 421 | else 422 | h.fail("NULL column not parsed correctly") 423 | end 424 | match m.columns(3)? 425 | | "" => None 426 | else 427 | h.fail("Empty string column not parsed correctly") 428 | end 429 | else 430 | h.fail("Wrong message returned.") 431 | end 432 | 433 | class \nodoc\ val _IncomingAuthenticationOkTestMessage 434 | let _bytes: Array[U8] val 435 | 436 | new val create() => 437 | let wb: Writer = Writer 438 | wb.u8(_MessageType.authentication_request()) 439 | wb.u32_be(8) 440 | wb.i32_be(_AuthenticationRequestType.ok()) 441 | 442 | _bytes = WriterToByteArray(wb) 443 | 444 | fun bytes(): Array[U8] val => 445 | _bytes 446 | 447 | class \nodoc\ val _IncomingAuthenticationMD5PasswordTestMessage 448 | let _bytes: Array[U8] val 449 | 450 | new val create(salt: String) => 451 | let wb: Writer = Writer 452 | wb.u8(_MessageType.authentication_request()) 453 | wb.u32_be(12) 454 | wb.i32_be(_AuthenticationRequestType.md5_password()) 455 | wb.write(salt) 456 | 457 | _bytes = WriterToByteArray(wb) 458 | 459 | fun bytes(): Array[U8] val => 460 | _bytes 461 | 462 | class \nodoc\ val _IncomingErrorResponseTestMessage 463 | let _bytes: Array[U8] val 464 | 465 | new val create(severity: String, code: String, message: String) => 466 | let payload_size = 4 + 467 | 1 + severity.size() + 1 + 468 | 1 + code.size() + 1 + 469 | 1 + message.size() + 1 + 470 | 1 471 | 472 | let wb: Writer = Writer 473 | wb.u8(_MessageType.error_response()) 474 | wb.u32_be(payload_size.u32()) 475 | wb.u8('S') 476 | wb.write(severity) 477 | wb.u8(0) 478 | wb.u8('C') 479 | wb.write(code) 480 | wb.u8(0) 481 | wb.u8('M') 482 | wb.write(message) 483 | wb.u8(0) 484 | wb.u8(0) 485 | 486 | _bytes = WriterToByteArray(wb) 487 | 488 | fun bytes(): Array[U8] val => 489 | _bytes 490 | 491 | class \nodoc\ val _IncomingJunkTestMessage 492 | """ 493 | Creates a junk message where "junk" is currently defined as having a message 494 | type that we don't recognize, aka not an ascii letter. 495 | """ 496 | let _bytes: Array[U8] val 497 | 498 | new val create() => 499 | let rand = Rand 500 | let wb: Writer = Writer 501 | wb.u8(1) 502 | wb.u32_be(7669) 503 | for i in Range(0, 100_000) do 504 | wb.u32_be(rand.u32()) 505 | end 506 | 507 | _bytes = WriterToByteArray(wb) 508 | 509 | fun bytes(): Array[U8] val => 510 | _bytes 511 | 512 | class \nodoc\ val _IncomingReadyForQueryTestMessage 513 | let _bytes: Array[U8] val 514 | 515 | new val create(status: U8) => 516 | let wb: Writer = Writer 517 | wb.u8(_MessageType.ready_for_query()) 518 | wb.u32_be(5) 519 | wb.u8(status) 520 | 521 | _bytes = WriterToByteArray(wb) 522 | 523 | fun bytes(): Array[U8] val => 524 | _bytes 525 | 526 | class \nodoc\ val _IncomingEmptyQueryResponseTestMessage 527 | let _bytes: Array[U8] val 528 | 529 | new val create() => 530 | let wb: Writer = Writer 531 | wb.u8(_MessageType.empty_query_response()) 532 | wb.u32_be(4) 533 | 534 | _bytes = WriterToByteArray(wb) 535 | 536 | fun bytes(): Array[U8] val => 537 | _bytes 538 | 539 | class \nodoc\ val _IncomingCommandCompleteTestMessage 540 | let _bytes: Array[U8] val 541 | 542 | new val create(command: String) => 543 | let payload_size = 4 + command.size() + 1 544 | let wb: Writer = Writer 545 | wb.u8(_MessageType.command_complete()) 546 | wb.u32_be(payload_size.u32()) 547 | wb.write(command) 548 | wb.u8(0) 549 | 550 | _bytes = WriterToByteArray(wb) 551 | 552 | fun bytes(): Array[U8] val => 553 | _bytes 554 | 555 | class \nodoc\ val _IncomingDataRowTestMessage 556 | let _bytes: Array[U8] val 557 | 558 | new val create(columns: Array[(String | None)] val) => 559 | let number_of_columns = columns.size() 560 | var payload_size: USize = 4 + 2 561 | let wb: Writer = Writer 562 | wb.u8(_MessageType.data_row()) 563 | // placeholder 564 | wb.u32_be(0) 565 | wb.u16_be(number_of_columns.u16()) 566 | for column in columns.values() do 567 | match column 568 | | None => 569 | wb.u32_be(-1) 570 | payload_size = payload_size + 4 571 | | "" => 572 | wb.u32_be(0) 573 | payload_size = payload_size + 4 574 | | let c: String => 575 | wb.u32_be(c.size().u32()) 576 | wb.write(c) 577 | payload_size = payload_size + 4 + c.size() 578 | end 579 | end 580 | 581 | // bytes with placeholder for length 582 | let b = WriterToByteArray(wb) 583 | // bytes for payload 584 | let pw: Writer = Writer.>u32_be(payload_size.u32()) 585 | let pb = WriterToByteArray(pw) 586 | // copy in payload size 587 | _bytes = recover val b.clone().>copy_from(pb, 0, 1, 4) end 588 | 589 | fun bytes(): Array[U8] val => 590 | _bytes 591 | 592 | class \nodoc\ val _IncomingRowDescriptionTestMessage 593 | let _bytes: Array[U8] val 594 | 595 | new val create(columns: Array[(String, String)] val) ? => 596 | let number_of_columns = columns.size() 597 | var payload_size: USize = 4 + 2 598 | let wb: Writer = Writer 599 | wb.u8(_MessageType.row_description()) 600 | // placeholder 601 | wb.u32_be(0) 602 | wb.u16_be(number_of_columns.u16()) 603 | for column in columns.values() do 604 | let name: String = column._1 605 | let column_type: U32 = match column._2 606 | | "text" => 25 607 | | "bool" => 16 608 | | "int2" => 21 609 | | "int4" => 23 610 | | "int8" => 20 611 | | "float4" => 700 612 | | "float8" => 701 613 | else 614 | error 615 | end 616 | // column name size and null terminator plus additional fields 617 | payload_size = payload_size + name.size() + 1 + 18 618 | wb.write(name) 619 | wb.u8(0) 620 | // currently unused in the parser 621 | wb.u32_be(0) 622 | // currently unused in the parser 623 | wb.u16_be(0) 624 | wb.u32_be(column_type) 625 | // currently unused in the parser 626 | wb.u16_be(0) 627 | wb.u32_be(0) 628 | wb.u16_be(0) 629 | end 630 | 631 | // bytes with placeholder for length 632 | let b = WriterToByteArray(wb) 633 | // bytes for payload 634 | let pw: Writer = Writer.>u32_be(payload_size.u32()) 635 | let pb = WriterToByteArray(pw) 636 | // copy in payload size 637 | _bytes = recover val b.clone().>copy_from(pb, 0, 1, 4) end 638 | 639 | fun bytes(): Array[U8] val => 640 | _bytes 641 | 642 | primitive WriterToByteArray 643 | fun apply(writer: Writer): Array[U8] val => 644 | recover val 645 | let out = Array[U8] 646 | for b in writer.done().values() do 647 | out.append(b) 648 | end 649 | out 650 | end 651 | --------------------------------------------------------------------------------