├── .formatter.exs ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── lib ├── postgrex.ex └── postgrex │ ├── app.ex │ ├── binary_extension.ex │ ├── binary_utils.ex │ ├── builtins.ex │ ├── default_types.ex │ ├── errcodes.txt │ ├── error.ex │ ├── error_code.ex │ ├── extension.ex │ ├── extensions │ ├── array.ex │ ├── bit_string.ex │ ├── bool.ex │ ├── box.ex │ ├── circle.ex │ ├── date.ex │ ├── float4.ex │ ├── float8.ex │ ├── hstore.ex │ ├── inet.ex │ ├── int2.ex │ ├── int4.ex │ ├── int8.ex │ ├── interval.ex │ ├── json.ex │ ├── jsonb.ex │ ├── line.ex │ ├── line_segment.ex │ ├── lquery.ex │ ├── ltree.ex │ ├── ltxtquery.ex │ ├── macaddr.ex │ ├── multirange.ex │ ├── name.ex │ ├── numeric.ex │ ├── oid.ex │ ├── path.ex │ ├── point.ex │ ├── polygon.ex │ ├── range.ex │ ├── raw.ex │ ├── record.ex │ ├── tid.ex │ ├── time.ex │ ├── timestamp.ex │ ├── timestamptz.ex │ ├── timetz.ex │ ├── tsvector.ex │ ├── uuid.ex │ ├── void_binary.ex │ ├── void_text.ex │ └── xid8.ex │ ├── messages.ex │ ├── notifications.ex │ ├── parameters.ex │ ├── protocol.ex │ ├── query.ex │ ├── replication_connection.ex │ ├── result.ex │ ├── scram.ex │ ├── scram │ └── locked_cache.ex │ ├── simple_connection.ex │ ├── stream.ex │ ├── super_extension.ex │ ├── type_info.ex │ ├── type_module.ex │ ├── type_server.ex │ ├── type_supervisor.ex │ ├── types.ex │ └── utils.ex ├── mix.exs ├── mix.lock └── test ├── alter_test.exs ├── builtins_test.exs ├── calendar_test.exs ├── client_test.exs ├── custom_extensions_test.exs ├── error_code_test.exs ├── error_test.exs ├── login_test.exs ├── notification_test.exs ├── postgrex_test.exs ├── query_test.exs ├── replication_connection_test.exs ├── schema_test.exs ├── scram └── locked_cache_test.exs ├── simple_connection_test.exs ├── stream_test.exs ├── test_helper.exs ├── transaction_test.exs ├── tsvector_test.exs ├── type_module_test.exs ├── type_server_test.exs ├── utils └── envs_test.exs └── utils_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Tell us about something that's not working the way we (probably) intend. 3 | labels: ["Kind:Bug", "State:Triage"] 4 | body: 5 | - type: input 6 | id: elixir-version 7 | attributes: 8 | label: Elixir version 9 | description: Use `elixir -v` to find the Elixir version. 10 | validations: 11 | required: true 12 | 13 | - type: input 14 | id: db-version 15 | attributes: 16 | label: Database and Version 17 | description: > 18 | The database and its version, i.e. PostgreSQL 9.4 19 | validations: 20 | required: true 21 | 22 | - type: input 23 | id: postgrex-version 24 | attributes: 25 | label: Postgrex Version 26 | description: Use `mix deps` to find the dependency version. 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: current-behavior 32 | attributes: 33 | label: Current behavior 34 | description: How can we reproduce what you're seeing? Include code samples, errors and stacktraces if appropriate. 35 | placeholder: |- 36 | 1. foo 37 | 2. bar 38 | 3. baz 39 | validations: 40 | required: true 41 | 42 | - type: textarea 43 | id: expected-behavior 44 | attributes: 45 | label: Expected behavior 46 | validations: 47 | required: true 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Discuss proposals 5 | url: https://groups.google.com/g/elixir-ecto 6 | about: Send proposals for new ideas in the mailing list. 7 | 8 | - name: Ask Questions 9 | url: https://elixirforum.com/tag/ecto 10 | about: Ask and answer questions on ElixirForum. 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-22.04 12 | 13 | services: 14 | pg: 15 | image: postgres:${{ matrix.pg.version }} 16 | env: 17 | POSTGRES_USER: postgres 18 | POSTGRES_PASSWORD: postgres 19 | POSTGRES_DB: postgres 20 | options: >- 21 | --health-cmd pg_isready 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 10 25 | ports: 26 | - 5432:5432 27 | volumes: 28 | - /var/run/postgresql:/var/run/postgresql 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | pg: 34 | - version: 9.4 35 | skip_wal: skip_wal 36 | - version: 9.5 37 | skip_wal: skip_wal 38 | - version: 9.6 39 | skip_wal: skip_wal 40 | - version: 10 41 | - version: 11 42 | - version: 12 43 | - version: 13 44 | - version: 14 45 | 46 | pair: 47 | - elixir: 1.13 48 | otp: 25.3 49 | include: 50 | - pg: 51 | version: 14 52 | pair: 53 | elixir: 1.18.1 54 | otp: 27.2 55 | lint: lint 56 | env: 57 | MIX_ENV: test 58 | steps: 59 | - name: "Set PG settings" 60 | run: | 61 | docker exec ${{ job.services.pg.id }} sh -c 'echo "wal_level=logical" >> /var/lib/postgresql/data/postgresql.conf' 62 | docker restart ${{ job.services.pg.id }} 63 | if: ${{ matrix.pg.skip_wal }} != 'skip_wal' 64 | 65 | - uses: actions/checkout@v2 66 | 67 | - uses: erlef/setup-beam@v1 68 | with: 69 | otp-version: ${{matrix.pair.otp}} 70 | elixir-version: ${{matrix.pair.elixir}} 71 | 72 | - uses: actions/cache@v4 73 | with: 74 | path: | 75 | deps 76 | _build 77 | key: ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}-${{ hashFiles('**/mix.lock') }} 78 | restore-keys: | 79 | ${{ runner.os }}-mix- 80 | 81 | - run: mix deps.get 82 | 83 | - run: mix format --check-formatted 84 | if: ${{ matrix.lint }} 85 | 86 | - run: mix deps.unlock --check-unused 87 | if: ${{ matrix.lint }} 88 | 89 | - run: mix deps.compile 90 | 91 | - run: mix compile --warnings-as-errors 92 | if: ${{ matrix.lint }} 93 | 94 | - run: mix test 95 | env: 96 | PGUSER: postgres 97 | PGPASSWORD: postgres 98 | PG_SOCKET_DIR: /var/run/postgresql 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /doc 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Postgrex 2 | 3 | [![Build Status](https://github.com/elixir-ecto/postgrex/workflows/CI/badge.svg)](https://github.com/elixir-ecto/postgrex/actions) 4 | 5 | PostgreSQL driver for Elixir. 6 | 7 | Documentation: http://hexdocs.pm/postgrex/ 8 | 9 | ## Examples 10 | 11 | ```iex 12 | iex> {:ok, pid} = Postgrex.start_link(hostname: "localhost", username: "postgres", password: "postgres", database: "postgres") 13 | {:ok, #PID<0.69.0>} 14 | 15 | iex> Postgrex.query!(pid, "SELECT user_id, text FROM comments", []) 16 | %Postgrex.Result{command: :select, empty?: false, columns: ["user_id", "text"], rows: [[3,"hey"],[4,"there"]], size: 2}} 17 | 18 | iex> Postgrex.query!(pid, "INSERT INTO comments (user_id, text) VALUES (10, 'heya')", []) 19 | %Postgrex.Result{command: :insert, columns: nil, rows: nil, num_rows: 1} 20 | ``` 21 | 22 | ## Features 23 | 24 | * Automatic decoding and encoding of Elixir values to and from PostgreSQL's binary format 25 | * User defined extensions for encoding and decoding any PostgreSQL type 26 | * Supports transactions, prepared queries and multiple pools via [DBConnection](https://github.com/elixir-ecto/db_connection) 27 | * Supports PostgreSQL 8.4, 9.0-9.6, and later (hstore is not supported on 8.4) 28 | 29 | ## Data representation 30 | 31 | | PostgreSQL | Elixir | 32 | |--------------------|---------------------------------------------------------------------------------------------------------------------------------------------| 33 | | `NULL` | `nil` | 34 | | `bool` | `true`, `false` | 35 | | `char` | `"é"` | 36 | | `int` | `42` | 37 | | `float` | `42.0` | 38 | | `text` | `"eric"` | 39 | | `bytea` | `<<42>>` | 40 | | `numeric` | `#Decimal<42.0>` (1) | 41 | | `date` | `%Date{year: 2013, month: 10, day: 12}` | 42 | | `time(tz)` | `%Time{hour: 0, minute: 37, second: 14}` (2) | 43 | | `timestamp` | `%NaiveDateTime{year: 2013, month: 10, day: 12, hour: 0, minute: 37, second: 14}` | 44 | | `timestamptz` | `%DateTime{year: 2013, month: 10, day: 12, hour: 0, minute: 37, second: 14, time_zone: "Etc/UTC"}` (2) | 45 | | `interval` | `%Postgrex.Interval{months: 14, days: 40, secs: 10920, microsecs: 315}` | 46 | | `interval` | `%Duration{month: 2, day: 5, second: 0, microsecond: {315, 6}}` (3) | 47 | | `array` | `[1, 2, 3]` | 48 | | `composite type` | `{42, "title", "content"}` | 49 | | `range` | `%Postgrex.Range{lower: 1, upper: 5}` | 50 | | `multirange` | `%Postgrex.Multirange{ranges: [%Postgrex.Range{lower: 1, upper: 5}, %Postgrex.Range{lower: 20, upper: 23}]}` | 51 | | `uuid` | `<<160,238,188,153,156,11,78,248,187,109,107,185,189,56,10,17>>` | 52 | | `hstore` | `%{"foo" => "bar"}` | 53 | | `oid types` | `42` | 54 | | `enum` | `"ok"` (4) | 55 | | `bit` | `<< 1::1, 0::1 >>` | 56 | | `varbit` | `<< 1::1, 0::1 >>` | 57 | | `tsvector` | `[%Postgrex.Lexeme{positions: [{1, :A}], word: "a"}]` | 58 | 59 | (1) [Decimal](http://github.com/ericmj/decimal) 60 | 61 | (2) Timezones will always be normalized to UTC or assumed to be UTC when no information is available, either by PostgreSQL or Postgrex 62 | 63 | (3) `%Duration{}` may only be used with Elixir 1.17+. Intervals will only be decoded into a `%Duration{}` struct if the option `interval_decode_type: Duration` is passed to `Postgrex.Types.define/3`. 64 | 65 | (4) Enumerated types (enum) are custom named database types with strings as values. 66 | 67 | (5) Anonymous composite types are decoded (read) as tuples but they cannot be encoded (written) to the database 68 | 69 | Postgrex does not automatically cast between types. For example, you can't pass a string where a date is expected. To add type casting, support new types, or change how any of the types above are encoded/decoded, you can use extensions. 70 | 71 | ## JSON support 72 | 73 | Postgrex comes with JSON support out of the box via the [Jason](https://github.com/michalmuskala/jason) library. To use it, add :jason to your dependencies: 74 | 75 | ```elixir 76 | {:jason, "~> 1.0"} 77 | ``` 78 | 79 | You can customize it to use another library via the `:json_library` configuration: 80 | 81 | ```elixir 82 | config :postgrex, :json_library, SomeOtherLib 83 | ``` 84 | 85 | Once you change the value, you have to recompile Postgrex, which can be done by cleaning its current build: 86 | 87 | ```sh 88 | mix deps.clean postgrex --build 89 | ``` 90 | 91 | ## Extensions 92 | 93 | Extensions are used to extend Postgrex' built-in type encoding/decoding. 94 | 95 | The [extensions](https://github.com/elixir-ecto/postgrex/blob/master/lib/postgrex/extensions/) directory in this project provides implementation for many Postgres' built-in data types. It is also a great example of how to implement your own extensions. For example, you can look at the [`Date`](https://github.com/elixir-ecto/postgrex/blob/master/lib/postgrex/extensions/date.ex) extension as a starting point. 96 | 97 | Once you defined your extensions, you should build custom type modules, passing all of your extensions as arguments: 98 | 99 | ```elixir 100 | Postgrex.Types.define(MyApp.PostgrexTypes, [MyApp.Postgis.Extensions], []) 101 | ``` 102 | 103 | `Postgrex.Types.define/3` must be called on its own file, outside of any module and function, as it only needs to be defined once during compilation. 104 | 105 | Once a type module is defined, you must specify it on `start_link`: 106 | 107 | ```elixir 108 | Postgrex.start_link(types: MyApp.PostgrexTypes) 109 | ``` 110 | 111 | ## OID type encoding 112 | 113 | PostgreSQL's wire protocol supports encoding types either as text or as binary. Unlike most client libraries Postgrex uses the binary protocol, not the text protocol. This allows for efficient encoding of types (e.g. 4-byte integers are encoded as 4 bytes, not as a string of digits) and automatic support for arrays and composite types. 114 | 115 | Unfortunately the PostgreSQL binary protocol transports [OID types](http://www.postgresql.org/docs/current/static/datatype-oid.html#DATATYPE-OID-TABLE) as integers while the text protocol transports them as string of their name, if one exists, and otherwise as integer. 116 | 117 | This means you either need to supply oid types as integers or perform an explicit cast (which would be automatic when using the text protocol) in the query. 118 | 119 | ```elixir 120 | # Fails since $1 is regclass not text. 121 | query("select nextval($1)", ["some_sequence"]) 122 | 123 | # Perform an explicit cast, this would happen automatically when using a 124 | # client library that uses the text protocol. 125 | query("select nextval($1::text::regclass)", ["some_sequence"]) 126 | 127 | # Determine the oid once and store it for later usage. This is the most 128 | # efficient way, since PostgreSQL only has to perform the lookup once. Client 129 | # libraries using the text protocol do not support this. 130 | %{rows: [{sequence_oid}]} = query("select $1::text::regclass", ["some_sequence"]) 131 | query("select nextval($1)", [sequence_oid]) 132 | ``` 133 | 134 | ## PgBouncer 135 | 136 | When using PgBouncer with transaction or statement pooling named prepared 137 | queries can not be used because the bouncer may route requests from the same 138 | postgrex connection to different PostgreSQL backend processes and discards named 139 | queries after the transactions closes. To force unnamed prepared queries: 140 | 141 | ```elixir 142 | Postgrex.start_link(prepare: :unnamed) 143 | ``` 144 | 145 | ## Contributing 146 | 147 | To contribute you need to compile Postgrex from source and test it: 148 | 149 | ``` 150 | $ git clone https://github.com/elixir-ecto/postgrex.git 151 | $ cd postgrex 152 | $ mix test 153 | ``` 154 | 155 | The tests requires some modifications to your [hba file](http://www.postgresql.org/docs/9.3/static/auth-pg-hba-conf.html). The path to it can be found by running `$ psql -U postgres -c "SHOW hba_file"` in your shell. Put the following above all other configurations (so that they override): 156 | 157 | ``` 158 | local all all trust 159 | host all postgrex_md5_pw 127.0.0.1/32 md5 160 | host all postgrex_cleartext_pw 127.0.0.1/32 password 161 | host all postgrex_scram_pw 127.0.0.1/32 scram-sha-256 162 | ``` 163 | 164 | The server needs to be restarted for the changes to take effect. Additionally you need to setup a PostgreSQL user with the same username as the local user and give it trust or ident in your hba file. Or you can export $PGUSER and $PGPASSWORD before running tests. 165 | 166 | ### Testing hstore on 9.0 167 | 168 | PostgreSQL versions 9.0 does not have the `CREATE EXTENSION` commands. This means we have to locate the postgres installation and run the `hstore.sql` in `contrib` to install `hstore`. Below is an example command to test 9.0 on OS X with homebrew installed postgres: 169 | 170 | ``` 171 | $ PGVERSION=9.0 PGPATH=/usr/local/share/postgresql9/ mix test 172 | ``` 173 | 174 | ## License 175 | 176 | Copyright 2013 Eric Meadows-Jönsson 177 | 178 | Licensed under the Apache License, Version 2.0 (the "License"); 179 | you may not use this file except in compliance with the License. 180 | You may obtain a copy of the License at 181 | 182 | http://www.apache.org/licenses/LICENSE-2.0 183 | 184 | Unless required by applicable law or agreed to in writing, software 185 | distributed under the License is distributed on an "AS IS" BASIS, 186 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 187 | See the License for the specific language governing permissions and 188 | limitations under the License. 189 | -------------------------------------------------------------------------------- /lib/postgrex/app.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.App do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_, _) do 6 | opts = [strategy: :one_for_one, name: Postgrex.Supervisor] 7 | 8 | children = [ 9 | {Postgrex.TypeSupervisor, :manager}, 10 | Postgrex.Parameters, 11 | Postgrex.SCRAM.LockedCache 12 | ] 13 | 14 | Supervisor.start_link(children, opts) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/postgrex/binary_extension.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.BinaryExtension do 2 | @moduledoc false 3 | 4 | defmacro __using__(matching) do 5 | quote location: :keep do 6 | @behaviour Postgrex.Extension 7 | 8 | def init(_), do: nil 9 | 10 | def matching(_), do: unquote(matching) 11 | 12 | def format(_), do: :binary 13 | 14 | defoverridable init: 1 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/postgrex/binary_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.BinaryUtils do 2 | @moduledoc false 3 | 4 | defmacro int64 do 5 | quote do: signed - 64 6 | end 7 | 8 | defmacro int32 do 9 | quote do: signed - 32 10 | end 11 | 12 | defmacro int16 do 13 | quote do: signed - 16 14 | end 15 | 16 | defmacro uint64 do 17 | quote do: unsigned - 64 18 | end 19 | 20 | defmacro uint32 do 21 | quote do: unsigned - 32 22 | end 23 | 24 | defmacro uint16 do 25 | quote do: unsigned - 16 26 | end 27 | 28 | defmacro int8 do 29 | quote do: signed - 8 30 | end 31 | 32 | defmacro float64 do 33 | quote do: float - 64 34 | end 35 | 36 | defmacro float32 do 37 | quote do: float - 32 38 | end 39 | 40 | defmacro binary(size) do 41 | quote do: binary - size(unquote(size)) 42 | end 43 | 44 | defmacro binary(size, unit) do 45 | quote do: binary - size(unquote(size)) - unit(unquote(unit)) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/postgrex/builtins.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Interval do 2 | @moduledoc """ 3 | Struct for PostgreSQL `interval`. 4 | 5 | ## Fields 6 | 7 | * `months` 8 | * `days` 9 | * `secs` 10 | * `microsecs` 11 | 12 | """ 13 | 14 | @type t :: %__MODULE__{months: integer, days: integer, secs: integer, microsecs: integer} 15 | 16 | defstruct months: 0, days: 0, secs: 0, microsecs: 0 17 | 18 | def compare( 19 | %__MODULE__{months: m1, days: d1, secs: s1, microsecs: ms1}, 20 | %__MODULE__{months: m2, days: d2, secs: s2, microsecs: ms2} 21 | ) do 22 | t1 = {m1, d1, s1, ms1} 23 | t2 = {m2, d2, s2, ms2} 24 | 25 | cond do 26 | t1 > t2 -> :gt 27 | t1 < t2 -> :lt 28 | true -> :eq 29 | end 30 | end 31 | 32 | def to_string(%__MODULE__{months: months, days: days, secs: secs, microsecs: microsecs}) do 33 | optional_interval(months, :month) <> 34 | optional_interval(days, :day) <> 35 | Integer.to_string(secs) <> 36 | optional_microsecs(microsecs) <> 37 | " seconds" 38 | end 39 | 40 | defp optional_interval(0, _), do: "" 41 | defp optional_interval(1, key), do: "1 #{key}, " 42 | defp optional_interval(n, key), do: "#{n} #{key}s, " 43 | 44 | defp optional_microsecs(0), 45 | do: "" 46 | 47 | defp optional_microsecs(ms), 48 | do: "." <> (ms |> Integer.to_string() |> String.pad_leading(6, "0")) 49 | end 50 | 51 | defmodule Postgrex.Range do 52 | @moduledoc """ 53 | Struct for PostgreSQL `range`. 54 | 55 | Note that PostgreSQL itself does not return ranges exactly as stored: 56 | `SELECT '(1,5)'::int4range` returns `[2,5)`, which is equivalent in terms 57 | of the values included in the range ([PostgreSQL docs](https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-IO)). 58 | When selecting data, this struct simply reflects what PostgreSQL returns. 59 | 60 | ## Fields 61 | 62 | * `lower` 63 | * `upper` 64 | * `lower_inclusive` 65 | * `upper_inclusive` 66 | 67 | """ 68 | 69 | @type t :: %__MODULE__{ 70 | lower: term | :empty | :unbound, 71 | upper: term | :empty | :unbound, 72 | lower_inclusive: boolean, 73 | upper_inclusive: boolean 74 | } 75 | 76 | defstruct lower: nil, upper: nil, lower_inclusive: true, upper_inclusive: true 77 | end 78 | 79 | defmodule Postgrex.Multirange do 80 | @moduledoc """ 81 | Struct for PostgreSQL `multirange`. 82 | 83 | ## Fields 84 | 85 | * `ranges` 86 | 87 | """ 88 | 89 | @type t :: %__MODULE__{ranges: [Postgrex.Range.t()]} 90 | 91 | defstruct ranges: nil 92 | end 93 | 94 | defmodule Postgrex.INET do 95 | @moduledoc """ 96 | Struct for PostgreSQL `inet` / `cidr`. 97 | 98 | ## Fields 99 | 100 | * `address` 101 | * `netmask` 102 | 103 | """ 104 | 105 | @type t :: %__MODULE__{address: :inet.ip_address(), netmask: nil | 0..128} 106 | 107 | defstruct address: nil, netmask: nil 108 | end 109 | 110 | defmodule Postgrex.MACADDR do 111 | @moduledoc """ 112 | Struct for PostgreSQL `macaddr`. 113 | 114 | ## Fields 115 | 116 | * `address` 117 | 118 | """ 119 | 120 | @type macaddr :: {0..255, 0..255, 0..255, 0..255, 0..255, 0..255} 121 | 122 | @type t :: %__MODULE__{address: macaddr} 123 | 124 | defstruct address: nil 125 | end 126 | 127 | defmodule Postgrex.Point do 128 | @moduledoc """ 129 | Struct for PostgreSQL `point`. 130 | 131 | ## Fields 132 | 133 | * `x` 134 | * `y` 135 | 136 | """ 137 | 138 | @type t :: %__MODULE__{x: float, y: float} 139 | 140 | defstruct x: nil, y: nil 141 | end 142 | 143 | defmodule Postgrex.Polygon do 144 | @moduledoc """ 145 | Struct for PostgreSQL `polygon`. 146 | 147 | ## Fields 148 | 149 | * `vertices` 150 | 151 | """ 152 | 153 | @type t :: %__MODULE__{vertices: [Postgrex.Point.t()]} 154 | 155 | defstruct vertices: nil 156 | end 157 | 158 | defmodule Postgrex.Line do 159 | @moduledoc """ 160 | Struct for PostgreSQL `line`. 161 | 162 | Note, lines are stored in PostgreSQL in the form `{a, b, c}`, which 163 | parameterizes a line as `a*x + b*y + c = 0`. 164 | 165 | ## Fields 166 | 167 | * `a` 168 | * `b` 169 | * `c` 170 | 171 | """ 172 | 173 | @type t :: %__MODULE__{a: float, b: float, c: float} 174 | 175 | defstruct a: nil, b: nil, c: nil 176 | end 177 | 178 | defmodule Postgrex.LineSegment do 179 | @moduledoc """ 180 | Struct for PostgreSQL `lseg`. 181 | 182 | ## Fields 183 | 184 | * `point1` 185 | * `point2` 186 | 187 | """ 188 | 189 | @type t :: %__MODULE__{point1: Postgrex.Point.t(), point2: Postgrex.Point.t()} 190 | 191 | defstruct point1: nil, point2: nil 192 | end 193 | 194 | defmodule Postgrex.Box do 195 | @moduledoc """ 196 | Struct for PostgreSQL `box`. 197 | 198 | ## Fields 199 | 200 | * `upper_right` 201 | * `bottom_left` 202 | 203 | """ 204 | 205 | @type t :: %__MODULE__{ 206 | upper_right: Postgrex.Point.t(), 207 | bottom_left: Postgrex.Point.t() 208 | } 209 | 210 | defstruct upper_right: nil, bottom_left: nil 211 | end 212 | 213 | defmodule Postgrex.Path do 214 | @moduledoc """ 215 | Struct for PostgreSQL `path`. 216 | 217 | ## Fields 218 | 219 | * `open` 220 | * `points` 221 | 222 | """ 223 | 224 | @type t :: %__MODULE__{points: [Postgrex.Point.t()], open: boolean} 225 | 226 | defstruct points: nil, open: nil 227 | end 228 | 229 | defmodule Postgrex.Circle do 230 | @moduledoc """ 231 | Struct for PostgreSQL `circle`. 232 | 233 | ## Fields 234 | 235 | * `center` 236 | * `radius` 237 | 238 | """ 239 | @type t :: %__MODULE__{center: Postgrex.Point.t(), radius: number} 240 | 241 | defstruct center: nil, radius: nil 242 | end 243 | 244 | defmodule Postgrex.Lexeme do 245 | @moduledoc """ 246 | Struct for PostgreSQL `lexeme`. 247 | 248 | ## Fields 249 | 250 | * `word` 251 | * `positions` 252 | 253 | """ 254 | 255 | @type t :: %__MODULE__{word: String.t(), positions: [{pos_integer, :A | :B | :C | nil}]} 256 | 257 | defstruct word: nil, positions: nil 258 | end 259 | -------------------------------------------------------------------------------- /lib/postgrex/default_types.ex: -------------------------------------------------------------------------------- 1 | Postgrex.Types.define(Postgrex.DefaultTypes, [], 2 | moduledoc: """ 3 | The default module used to encode/decode PostgreSQL types. 4 | 5 | Type modules are given as the `:types` option in `Postgrex.start_link/1`. 6 | """ 7 | ) 8 | -------------------------------------------------------------------------------- /lib/postgrex/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Error do 2 | defexception [:message, :postgres, :connection_id, :query] 3 | 4 | @type t :: %Postgrex.Error{} 5 | 6 | @metadata [:table, :column, :constraint, :hint] 7 | 8 | def exception(opts) do 9 | postgres = 10 | if fields = Keyword.get(opts, :postgres) do 11 | code = fields.code 12 | 13 | fields 14 | |> Map.put(:pg_code, code) 15 | |> Map.put(:code, Postgrex.ErrorCode.code_to_name(code)) 16 | end 17 | 18 | message = Keyword.get(opts, :message) 19 | connection_id = Keyword.get(opts, :connection_id) 20 | %Postgrex.Error{postgres: postgres, message: message, connection_id: connection_id} 21 | end 22 | 23 | def message(e) do 24 | if map = e.postgres do 25 | IO.iodata_to_binary([ 26 | map.severity, 27 | ?\s, 28 | map.pg_code, 29 | ?\s, 30 | [?(, Atom.to_string(map.code), ?)], 31 | ?\s, 32 | map.message, 33 | build_query(e.query), 34 | build_metadata(map), 35 | build_detail(map) 36 | ]) 37 | else 38 | e.message 39 | end 40 | end 41 | 42 | defp build_query(nil), do: [] 43 | defp build_query(query), do: ["\n\n query: ", query] 44 | 45 | defp build_metadata(map) do 46 | metadata = for k <- @metadata, v = map[k], do: "\n #{k}: #{v}" 47 | 48 | case metadata do 49 | [] -> [] 50 | _ -> ["\n" | metadata] 51 | end 52 | end 53 | 54 | defp build_detail(%{detail: detail}) when is_binary(detail), do: ["\n\n" | detail] 55 | defp build_detail(_), do: [] 56 | end 57 | 58 | defmodule Postgrex.QueryError do 59 | defexception [:message] 60 | end 61 | -------------------------------------------------------------------------------- /lib/postgrex/error_code.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.ErrorCode do 2 | @moduledoc false 3 | # We put this file in the repo because the last real change happened in 2011. 4 | # https://github.com/postgres/postgres/blob/master/src/backend/utils/errcodes.txt 5 | @external_resource errcodes_path = Path.join(__DIR__, "errcodes.txt") 6 | 7 | errcodes = 8 | for line <- File.stream!(errcodes_path), 9 | match?(<<_code::5*8, " ", _::binary>>, line) do 10 | case String.split(line, " ", trim: true) do 11 | [code, _, _, name] -> {code, name |> String.trim() |> String.to_atom()} 12 | # duplicated code without name 13 | [code, _, _] -> {code} 14 | end 15 | end 16 | 17 | {errcodes, duplicates} = Enum.split_with(errcodes, &match?({_, _}, &1)) 18 | 19 | # The errcodes.txt file does contain some codes twice, but the duplicates 20 | # don't have a name. Make sure every code without a name has another 21 | # entry with a name. 22 | for {duplicate} <- duplicates do 23 | unless Enum.find(errcodes, fn {code, _} -> code == duplicate end) do 24 | raise RuntimeError, "found errcode #{duplicate} without name" 25 | end 26 | end 27 | 28 | @doc ~S""" 29 | Translates a PostgreSQL error code into a name 30 | 31 | Examples: 32 | iex> code_to_name("23505") 33 | :unique_violation 34 | """ 35 | @spec code_to_name(String.t()) :: atom | no_return 36 | def code_to_name(code) 37 | 38 | for {code, errcodes} <- Enum.group_by(errcodes, &elem(&1, 0)) do 39 | [{^code, name}] = errcodes 40 | def code_to_name(unquote(code)), do: unquote(name) 41 | end 42 | 43 | def code_to_name(_), do: nil 44 | 45 | @doc ~S""" 46 | Translates a PostgreSQL error name into a list of possible codes. 47 | Most error names have only a single code, but there are exceptions. 48 | 49 | Examples: 50 | iex> name_to_code(:prohibited_sql_statement_attempted) 51 | "2F003" 52 | """ 53 | @spec name_to_code(atom) :: String.t() 54 | def name_to_code(name) 55 | 56 | @code_decision_table [ 57 | # 01004 not used 58 | string_data_right_truncation: "22001", 59 | # 38002 or 2F002 not used 60 | modifying_sql_data_not_permitted: nil, 61 | # 38003 not used 62 | prohibited_sql_statement_attempted: "2F003", 63 | # 38004 or 2F004 not used 64 | reading_sql_data_not_permitted: nil, 65 | # 39004 not used 66 | null_value_not_allowed: "22004" 67 | ] 68 | 69 | for {name, errcodes} <- Enum.group_by(errcodes, &elem(&1, 1)) do 70 | case Keyword.fetch(@code_decision_table, name) do 71 | {:ok, nil} -> 72 | :ok 73 | 74 | {:ok, code} -> 75 | def name_to_code(unquote(name)), do: unquote(code) 76 | 77 | :error -> 78 | [{code, ^name}] = errcodes 79 | def name_to_code(unquote(name)), do: unquote(code) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/postgrex/extension.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extension do 2 | @moduledoc """ 3 | An extension knows how to encode and decode PostgreSQL types to and 4 | from Elixir values. 5 | 6 | Custom extensions can be enabled via `Postgrex.Types.define/3`. 7 | `Postgrex.Types.define/3` must be called on its own file, outside of 8 | any module and function, as it only needs to be defined once during 9 | compilation. 10 | 11 | For example to support label trees using the text encoding format: 12 | 13 | defmodule MyApp.LTree do 14 | @behaviour Postgrex.Extension 15 | 16 | # It can be memory efficient to copy the decoded binary because a 17 | # reference counted binary that points to a larger binary will be passed 18 | # to the decode/4 callback. Copying the binary can allow the larger 19 | # binary to be garbage collected sooner if the copy is going to be kept 20 | # for a longer period of time. See `:binary.copy/1` for more 21 | # information. 22 | def init(opts) do 23 | Keyword.get(opts, :decode_copy, :copy) 24 | end 25 | 26 | # Use this extension when `type` from %Postgrex.TypeInfo{} is "ltree" 27 | def matching(_state), do: [type: "ltree"] 28 | 29 | # Use the text format, "ltree" does not have a binary format. 30 | def format(_state), do: :text 31 | 32 | # Use quoted expression to encode a string that is the same as 33 | # postgresql's ltree text format. The quoted expression should contain 34 | # clauses that match those of a `case` or `fn`. Encoding matches on the 35 | # value and returns encoded `iodata()`. The first 4 bytes in the 36 | # `iodata()` must be the byte size of the rest of the encoded data, as a 37 | # signed 32bit big endian integer. 38 | def encode(_state) do 39 | quote do 40 | bin when is_binary(bin) -> 41 | [<> | bin] 42 | end 43 | end 44 | 45 | # Use quoted expression to decode the data to a string. Decoding matches 46 | # on an encoded binary with the same signed 32bit big endian integer 47 | # length header. 48 | def decode(:reference) do 49 | quote do 50 | <> -> 51 | bin 52 | end 53 | end 54 | def decode(:copy) do 55 | quote do 56 | <> -> 57 | :binary.copy(bin) 58 | end 59 | end 60 | end 61 | 62 | This example could be used in a custom types module: 63 | 64 | Postgrex.Types.define(MyApp.Types, [MyApp.LTree]) 65 | 66 | Or pass in opts for the extension that will be passed to the `init/1` callback: 67 | 68 | Postgrex.Types.define(MyApp.Types, [{MyApp.LTree, [decode_copy: :copy]}]) 69 | 70 | """ 71 | 72 | @type t :: module 73 | @type state :: term 74 | 75 | @doc """ 76 | Should perform any initialization of the extension. The function receives the 77 | user options. The state returned from this function will be passed to other 78 | callbacks. 79 | """ 80 | @callback init(Keyword.t()) :: state 81 | 82 | @doc """ 83 | Prelude defines properties and values that are attached to the body of 84 | the types module. 85 | """ 86 | @callback prelude(state) :: Macro.t() 87 | 88 | @doc """ 89 | Specifies the types the extension matches, see `Postgrex.TypeInfo` for 90 | specification of the fields. 91 | """ 92 | @callback matching(state) :: [ 93 | type: String.t(), 94 | send: String.t(), 95 | receive: String.t(), 96 | input: String.t(), 97 | output: String.t() 98 | ] 99 | 100 | @doc """ 101 | Returns the format the type should be encoded as. See 102 | http://www.postgresql.org/docs/9.4/static/protocol-overview.html#PROTOCOL-FORMAT-CODES. 103 | """ 104 | @callback format(state) :: :binary | :text 105 | 106 | @doc """ 107 | Returns a quoted list of clauses that encode an Elixir value to iodata. 108 | 109 | It must use a signed 32 bit big endian integer byte length header. 110 | 111 | def encode(_) do 112 | quote do 113 | integer -> 114 | <<8::signed-32, integer::signed-64>> 115 | end 116 | end 117 | 118 | """ 119 | @callback encode(state) :: Macro.t() 120 | 121 | @doc """ 122 | Returns a quoted list of clauses that decode a binary to an Elixir value. 123 | 124 | The pattern must use binary syntax and decode a fixed length using the signed 125 | 32 bit big endian integer byte length header. 126 | 127 | def decode(_) do 128 | quote do 129 | # length header is in bytes 130 | <> -> 131 | integer 132 | end 133 | end 134 | """ 135 | @callback decode(state) :: Macro.t() 136 | 137 | @optional_callbacks [prelude: 1] 138 | end 139 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/array.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Array do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | @behaviour Postgrex.SuperExtension 5 | 6 | def init(_), do: nil 7 | 8 | def matching(_), 9 | do: [send: "array_send"] 10 | 11 | def format(_), 12 | do: :super_binary 13 | 14 | def oids(%Postgrex.TypeInfo{array_elem: elem_oid}, _), 15 | do: [elem_oid] 16 | 17 | def encode(_) do 18 | quote location: :keep do 19 | list, [oid], [type] when is_list(list) -> 20 | # encode_list/2 defined by TypeModule 21 | encoder = &encode_list(&1, type) 22 | unquote(__MODULE__).encode(list, oid, encoder) 23 | 24 | other, _, _ -> 25 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a list") 26 | end 27 | end 28 | 29 | def decode(_) do 30 | quote location: :keep do 31 | <>, [oid], [type] -> 32 | <> = binary 34 | 35 | # decode_list/2 defined by TypeModule 36 | sub_type_with_mod = 37 | case type do 38 | {extension, sub_oids, sub_types} -> {extension, sub_oids, sub_types, var!(mod)} 39 | extension -> {extension, var!(mod)} 40 | end 41 | 42 | flat = decode_list(data, sub_type_with_mod) 43 | 44 | unquote(__MODULE__).decode(dims, flat) 45 | end 46 | end 47 | 48 | ## Helpers 49 | 50 | # Special case for empty lists. This treats an empty list as an empty 1-dim array. 51 | # While libpq will decode a payload encoded for a 0-dim array, CockroachDB will not. 52 | # Also, this is how libpq actually encodes 0-dim arrays. 53 | def encode([], elem_oid, _encoder) do 54 | <<20::int32(), 1::int32(), 0::int32(), elem_oid::uint32(), 0::int32(), 1::int32()>> 55 | end 56 | 57 | def encode(list, elem_oid, encoder) do 58 | {data, ndims, lengths} = encode(list, 0, [], encoder) 59 | lengths = for len <- Enum.reverse(lengths), do: <> 60 | iodata = [<>, lengths, data] 61 | [<> | iodata] 62 | end 63 | 64 | defp encode([], ndims, lengths, _encoder) do 65 | {"", ndims, lengths} 66 | end 67 | 68 | defp encode([head | tail] = list, ndims, lengths, encoder) when is_list(head) do 69 | lengths = [length(list) | lengths] 70 | {data, ndims, lengths} = encode(head, ndims, lengths, encoder) 71 | [dimlength | _] = lengths 72 | 73 | rest = 74 | Enum.reduce(tail, [], fn sublist, acc -> 75 | {data, _, [len | _]} = encode(sublist, ndims, lengths, encoder) 76 | 77 | if len != dimlength do 78 | raise ArgumentError, "nested lists must have lists with matching lengths" 79 | end 80 | 81 | [acc | data] 82 | end) 83 | 84 | {[data | rest], ndims + 1, lengths} 85 | end 86 | 87 | defp encode(list, ndims, lengths, encoder) do 88 | {encoder.(list), ndims + 1, [length(list) | lengths]} 89 | end 90 | 91 | def decode(dims, elems) do 92 | case decode_dims(dims, []) do 93 | [] when elems == [] -> 94 | [] 95 | 96 | [length] when length(elems) == length -> 97 | Enum.reverse(elems) 98 | 99 | lengths -> 100 | {array, []} = nest(elems, lengths) 101 | array 102 | end 103 | end 104 | 105 | defp decode_dims(<>, acc) do 106 | decode_dims(rest, [len | acc]) 107 | end 108 | 109 | defp decode_dims(<<>>, acc) do 110 | Enum.reverse(acc) 111 | end 112 | 113 | # elems and lengths in reverse order 114 | defp nest(elems, [len]) do 115 | nest_inner(elems, len, []) 116 | end 117 | 118 | defp nest(elems, [len | lengths]) do 119 | nest(elems, len, lengths, []) 120 | end 121 | 122 | defp nest(elems, 0, _, acc) do 123 | {acc, elems} 124 | end 125 | 126 | defp nest(elems, n, lengths, acc) do 127 | {row, elems} = nest(elems, lengths) 128 | nest(elems, n - 1, lengths, [row | acc]) 129 | end 130 | 131 | defp nest_inner(elems, 0, acc) do 132 | {acc, elems} 133 | end 134 | 135 | defp nest_inner([elem | elems], n, acc) do 136 | nest_inner(elems, n - 1, [elem | acc]) 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/bit_string.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.BitString do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "bit_send", send: "varbit_send" 5 | 6 | def init(opts), do: Keyword.fetch!(opts, :decode_binary) 7 | 8 | def encode(_) do 9 | quote location: :keep, generated: true do 10 | val when is_binary(val) -> 11 | [<> | val] 12 | 13 | val when is_bitstring(val) -> 14 | bin_size = byte_size(val) 15 | last_pos = bin_size - 1 16 | <> = val 17 | pad = 8 - bit_size(last) 18 | bit_count = bit_size(val) 19 | 20 | [ 21 | <>, 22 | binary 23 | | <> 24 | ] 25 | 26 | val -> 27 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(val, "a bitstring") 28 | end 29 | end 30 | 31 | def decode(:copy) do 32 | quote location: :keep do 33 | <> -> 34 | copy = :binary.copy(value) 35 | <> = copy 36 | bits 37 | end 38 | end 39 | 40 | def decode(:reference) do 41 | quote location: :keep do 42 | <> -> 43 | <> = value 44 | bits 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/bool.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Bool do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "boolsend" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | true -> 9 | <<1::int32(), 1>> 10 | 11 | false -> 12 | <<1::int32(), 0>> 13 | 14 | other -> 15 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a boolean") 16 | end 17 | end 18 | 19 | def decode(_) do 20 | quote location: :keep do 21 | <<1::int32(), 1>> -> true 22 | <<1::int32(), 0>> -> false 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/box.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Box do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "box_send" 5 | alias Postgrex.Extensions.Point 6 | 7 | def encode(_) do 8 | quote location: :keep, generated: true do 9 | %Postgrex.Box{upper_right: p1, bottom_left: p2} -> 10 | encoded_p1 = Point.encode_point(p1, Postgrex.Box) 11 | encoded_p2 = Point.encode_point(p2, Postgrex.Box) 12 | # 2 points -> 16 bytes each 13 | [<<32::int32()>>, encoded_p1 | encoded_p2] 14 | 15 | other -> 16 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Box) 17 | end 18 | end 19 | 20 | def decode(_) do 21 | quote location: :keep do 22 | # 2 points -> 16 bytes each 23 | <<32::int32(), x1::float64(), y1::float64(), x2::float64(), y2::float64()>> -> 24 | p1 = %Postgrex.Point{x: x1, y: y1} 25 | p2 = %Postgrex.Point{x: x2, y: y2} 26 | %Postgrex.Box{upper_right: p1, bottom_left: p2} 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/circle.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Circle do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "circle_send" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | %Postgrex.Circle{center: %Postgrex.Point{x: x, y: y}, radius: r} 9 | when is_number(x) and is_number(y) and is_number(r) and r >= 0 -> 10 | <<24::int32(), x::float64(), y::float64(), r::float64()>> 11 | 12 | other -> 13 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Circle) 14 | end 15 | end 16 | 17 | def decode(_) do 18 | quote location: :keep do 19 | <<24::int32(), x::float64(), y::float64(), r::float64()>> -> 20 | %Postgrex.Circle{center: %Postgrex.Point{x: x, y: y}, radius: r} 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/date.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Date do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "date_send" 5 | 6 | @gd_epoch Date.to_gregorian_days(~D[2000-01-01]) 7 | 8 | # Elixir supports earlier dates but this is the 9 | # earliest supported in Postgresql. 10 | @min_days Date.to_gregorian_days(~D[-4713-01-01]) 11 | 12 | def encode(_) do 13 | quote location: :keep do 14 | %Date{calendar: Calendar.ISO} = date -> 15 | unquote(__MODULE__).encode_elixir(date) 16 | 17 | other -> 18 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Date) 19 | end 20 | end 21 | 22 | def decode(_) do 23 | quote location: :keep do 24 | <<4::int32(), days::int32()>> -> 25 | unquote(__MODULE__).day_to_elixir(days) 26 | end 27 | end 28 | 29 | ## Helpers 30 | 31 | def encode_elixir(date) do 32 | <<4::int32(), Date.to_gregorian_days(date) - @gd_epoch::int32()>> 33 | end 34 | 35 | def day_to_elixir(days) do 36 | days = days + @gd_epoch 37 | 38 | if days > @min_days do 39 | Date.from_gregorian_days(days) 40 | else 41 | raise ArgumentError, 42 | "Postgrex can only decode dates with days after #{@min_days}, got: #{inspect(days)}" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/float4.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Float4 do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "float4send" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | n when is_number(n) -> 9 | <<4::int32(), n::float32()>> 10 | 11 | :NaN -> 12 | <<4::int32(), 0::1, 255, 1::1, 0::22>> 13 | 14 | :inf -> 15 | <<4::int32(), 0::1, 255, 0::23>> 16 | 17 | :"-inf" -> 18 | <<4::int32(), 1::1, 255, 0::23>> 19 | 20 | other -> 21 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a float") 22 | end 23 | end 24 | 25 | def decode(_) do 26 | quote location: :keep do 27 | <<4::int32(), 0::1, 255, 0::23>> -> :inf 28 | <<4::int32(), 1::1, 255, 0::23>> -> :"-inf" 29 | <<4::int32(), _::1, 255, _::23>> -> :NaN 30 | <<4::int32(), float::float32()>> -> float 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/float8.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Float8 do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "float8send" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | n when is_number(n) -> 9 | <<8::int32(), n::float64()>> 10 | 11 | :NaN -> 12 | <<8::int32(), 0::1, 2047::11, 1::1, 0::51>> 13 | 14 | :inf -> 15 | <<8::int32(), 0::1, 2047::11, 0::52>> 16 | 17 | :"-inf" -> 18 | <<8::int32(), 1::1, 2047::11, 0::52>> 19 | 20 | other -> 21 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a float") 22 | end 23 | end 24 | 25 | def decode(_) do 26 | quote location: :keep do 27 | <<8::int32(), 0::1, 2047::11, 0::52>> -> :inf 28 | <<8::int32(), 1::1, 2047::11, 0::52>> -> :"-inf" 29 | <<8::int32(), _::1, 2047::11, _::52>> -> :NaN 30 | <<8::int32(), float::float64()>> -> float 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/hstore.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.HStore do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, type: "hstore" 5 | 6 | def init(opts), do: Keyword.fetch!(opts, :decode_binary) 7 | 8 | def encode(_) do 9 | quote location: :keep do 10 | %{} = map -> 11 | data = unquote(__MODULE__).encode_hstore(map) 12 | [<> | data] 13 | 14 | other -> 15 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a map") 16 | end 17 | end 18 | 19 | def decode(mode) do 20 | quote do 21 | <> -> 22 | unquote(__MODULE__).decode_hstore(data, unquote(mode)) 23 | end 24 | end 25 | 26 | ## Helpers 27 | 28 | def encode_hstore(hstore_map) do 29 | keys_and_values = 30 | Enum.reduce(hstore_map, "", fn {key, value}, acc -> 31 | [acc, encode_hstore_key(key), encode_hstore_value(value)] 32 | end) 33 | 34 | [<> | keys_and_values] 35 | end 36 | 37 | defp encode_hstore_key(key) when is_binary(key) do 38 | encode_hstore_value(key) 39 | end 40 | 41 | defp encode_hstore_key(key) when is_nil(key) do 42 | raise ArgumentError, "hstore keys cannot be nil!" 43 | end 44 | 45 | defp encode_hstore_value(nil) do 46 | <<-1::int32()>> 47 | end 48 | 49 | defp encode_hstore_value(value) when is_binary(value) do 50 | value_byte_size = byte_size(value) 51 | <> <> value 52 | end 53 | 54 | def decode_hstore(<<_length::int32(), pairs::binary>>, :reference) do 55 | decode_hstore_ref(pairs, %{}) 56 | end 57 | 58 | def decode_hstore(<<_length::int32(), pairs::binary>>, :copy) do 59 | decode_hstore_copy(pairs, %{}) 60 | end 61 | 62 | defp decode_hstore_ref(<<>>, acc) do 63 | acc 64 | end 65 | 66 | # in the case of a NULL value, there won't be a length 67 | defp decode_hstore_ref( 68 | <>, 69 | acc 70 | ) do 71 | decode_hstore_ref(rest, Map.put(acc, key, nil)) 72 | end 73 | 74 | defp decode_hstore_ref( 75 | <>, 77 | acc 78 | ) do 79 | decode_hstore_ref(rest, Map.put(acc, key, value)) 80 | end 81 | 82 | defp decode_hstore_copy(<<>>, acc) do 83 | acc 84 | end 85 | 86 | # in the case of a NULL value, there won't be a length 87 | defp decode_hstore_copy( 88 | <>, 89 | acc 90 | ) do 91 | decode_hstore_copy(rest, Map.put(acc, :binary.copy(key), nil)) 92 | end 93 | 94 | defp decode_hstore_copy( 95 | <>, 97 | acc 98 | ) do 99 | decode_hstore_copy(rest, Map.put(acc, :binary.copy(key), :binary.copy(value))) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/inet.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.INET do 2 | @moduledoc false 3 | 4 | import Postgrex.BinaryUtils, warn: false 5 | use Postgrex.BinaryExtension, send: "cidr_send", send: "inet_send" 6 | 7 | def encode(_) do 8 | quote location: :keep do 9 | %Postgrex.INET{address: {a, b, c, d}, netmask: nil} -> 10 | <<8::int32(), 2, 32, 0, 4, a, b, c, d>> 11 | 12 | %Postgrex.INET{address: {a, b, c, d}, netmask: n} -> 13 | <<8::int32(), 2, n, 1, 4, a, b, c, d>> 14 | 15 | %Postgrex.INET{address: {a, b, c, d, e, f, g, h}, netmask: nil} -> 16 | <<20::int32(), 3, 128, 0, 16, a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>> 17 | 18 | %Postgrex.INET{address: {a, b, c, d, e, f, g, h}, netmask: n} -> 19 | <<20::int32(), 3, n, 1, 16, a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>> 20 | 21 | other -> 22 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.INET) 23 | end 24 | end 25 | 26 | def decode(_) do 27 | quote location: :keep do 28 | <<8::int32(), 2, n, cidr?, 4, a, b, c, d>> -> 29 | n = if(cidr? == 1 or n != 32, do: n, else: nil) 30 | %Postgrex.INET{address: {a, b, c, d}, netmask: n} 31 | 32 | <<20::int32(), 3, n, cidr?, 16, a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>> -> 33 | n = if(cidr? == 1 or n != 128, do: n, else: nil) 34 | %Postgrex.INET{address: {a, b, c, d, e, f, g, h}, netmask: n} 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/int2.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Int2 do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "int2send" 5 | 6 | @int2_range -32768..32767 7 | 8 | def encode(_) do 9 | range = Macro.escape(@int2_range) 10 | 11 | quote location: :keep do 12 | int when is_integer(int) and int in unquote(range) -> 13 | <<2::int32(), int::int16()>> 14 | 15 | other -> 16 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, unquote(range)) 17 | end 18 | end 19 | 20 | def decode(_) do 21 | quote location: :keep do 22 | <<2::int32(), int::int16()>> -> int 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/int4.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Int4 do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "int4send" 5 | 6 | @int4_range -2_147_483_648..2_147_483_647 7 | 8 | def encode(_) do 9 | range = Macro.escape(@int4_range) 10 | 11 | quote location: :keep do 12 | int when is_integer(int) and int in unquote(range) -> 13 | <<4::int32(), int::int32()>> 14 | 15 | other -> 16 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, unquote(range)) 17 | end 18 | end 19 | 20 | def decode(_) do 21 | quote location: :keep do 22 | <<4::int32(), int::int32()>> -> int 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/int8.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Int8 do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "int8send", send: "pg_lsn_send" 5 | 6 | @int8_range -9_223_372_036_854_775_808..9_223_372_036_854_775_807 7 | 8 | def encode(_) do 9 | range = Macro.escape(@int8_range) 10 | 11 | quote location: :keep do 12 | int when is_integer(int) and int in unquote(range) -> 13 | <<8::int32(), int::int64()>> 14 | 15 | other -> 16 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, unquote(range)) 17 | end 18 | end 19 | 20 | def decode(_) do 21 | quote location: :keep do 22 | <<8::int32(), int::int64()>> -> int 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/interval.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Interval do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "interval_send" 5 | 6 | def init(opts) do 7 | case Keyword.get(opts, :interval_decode_type, Postgrex.Interval) do 8 | type when type in [Postgrex.Interval, Duration] -> 9 | type 10 | 11 | other -> 12 | raise ArgumentError, 13 | "#{inspect(other)} is not valid for `:interval_decode_type`. Please use either `Postgrex.Interval` or `Duration`" 14 | end 15 | end 16 | 17 | if Code.ensure_loaded?(Duration) do 18 | import Bitwise, warn: false 19 | @default_precision 6 20 | @precision_mask 0xFFFF 21 | # 0xFFFF: user did not specify precision (2's complement version of -1) 22 | # nil: coming from a super type that does not pass modifier for sub-type 23 | @unspecified_precision [0xFFFF, nil] 24 | 25 | def encode(_) do 26 | quote location: :keep do 27 | %Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} -> 28 | microseconds = 1_000_000 * seconds + microseconds 29 | <<16::int32(), microseconds::int64(), days::int32(), months::int32()>> 30 | 31 | %Duration{ 32 | year: years, 33 | month: months, 34 | week: weeks, 35 | day: days, 36 | hour: hours, 37 | minute: minutes, 38 | second: seconds, 39 | microsecond: {microseconds, _precision} 40 | } -> 41 | months = 12 * years + months 42 | days = 7 * weeks + days 43 | microseconds = 1_000_000 * (3600 * hours + 60 * minutes + seconds) + microseconds 44 | <<16::int32(), microseconds::int64(), days::int32(), months::int32()>> 45 | 46 | other -> 47 | raise DBConnection.EncodeError, 48 | Postgrex.Utils.encode_msg(other, {Postgrex.Interval, Duration}) 49 | end 50 | end 51 | 52 | def decode(type) do 53 | quote location: :keep, generated: true do 54 | <<16::int32(), microseconds::int64(), days::int32(), months::int32()>> -> 55 | unquote(__MODULE__).decode_interval( 56 | microseconds, 57 | days, 58 | months, 59 | var!(mod), 60 | unquote(type) 61 | ) 62 | end 63 | end 64 | 65 | ## Helpers 66 | 67 | def decode_interval(microseconds, days, months, _type_mod, Postgrex.Interval) do 68 | seconds = div(microseconds, 1_000_000) 69 | microseconds = rem(microseconds, 1_000_000) 70 | 71 | %Postgrex.Interval{ 72 | months: months, 73 | days: days, 74 | secs: seconds, 75 | microsecs: microseconds 76 | } 77 | end 78 | 79 | def decode_interval(microseconds, days, months, type_mod, Duration) do 80 | seconds = div(microseconds, 1_000_000) 81 | microseconds = rem(microseconds, 1_000_000) 82 | precision = if type_mod, do: type_mod &&& unquote(@precision_mask) 83 | 84 | precision = 85 | if precision in unquote(@unspecified_precision), 86 | do: unquote(@default_precision), 87 | else: precision 88 | 89 | Duration.new!( 90 | month: months, 91 | day: days, 92 | second: seconds, 93 | microsecond: {microseconds, precision} 94 | ) 95 | end 96 | else 97 | def encode(_) do 98 | quote location: :keep do 99 | %Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} -> 100 | microseconds = 1_000_000 * seconds + microseconds 101 | <<16::int32(), microseconds::int64(), days::int32(), months::int32()>> 102 | 103 | other -> 104 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Interval) 105 | end 106 | end 107 | 108 | def decode(_) do 109 | quote location: :keep do 110 | <<16::int32(), microseconds::int64(), days::int32(), months::int32()>> -> 111 | unquote(__MODULE__).decode_interval(microseconds, days, months, Postgrex.Interval) 112 | end 113 | end 114 | 115 | ## Helpers 116 | 117 | def decode_interval(microseconds, days, months, Postgrex.Interval) do 118 | seconds = div(microseconds, 1_000_000) 119 | microseconds = rem(microseconds, 1_000_000) 120 | 121 | %Postgrex.Interval{ 122 | months: months, 123 | days: days, 124 | secs: seconds, 125 | microsecs: microseconds 126 | } 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.JSON do 2 | @moduledoc false 3 | @behaviour Postgrex.Extension 4 | import Postgrex.BinaryUtils, warn: false 5 | 6 | def init(opts) do 7 | json = 8 | Keyword.get_lazy(opts, :json, fn -> 9 | Application.get_env(:postgrex, :json_library, Jason) 10 | end) 11 | 12 | {json, Keyword.get(opts, :decode_binary, :copy)} 13 | end 14 | 15 | def matching({nil, _}), 16 | do: [] 17 | 18 | def matching(_), 19 | do: [type: "json"] 20 | 21 | def format(_), 22 | do: :binary 23 | 24 | def prelude({library, _}) do 25 | if library do 26 | quote do 27 | @compile {:no_warn_undefined, unquote(library)} 28 | end 29 | end 30 | end 31 | 32 | def encode({library, _}) do 33 | quote location: :keep do 34 | map -> 35 | data = unquote(library).encode_to_iodata!(map) 36 | [<> | data] 37 | end 38 | end 39 | 40 | def decode({library, :copy}) do 41 | quote location: :keep do 42 | <> -> 43 | json 44 | |> :binary.copy() 45 | |> unquote(library).decode!() 46 | end 47 | end 48 | 49 | def decode({library, :reference}) do 50 | quote location: :keep do 51 | <> -> 52 | unquote(library).decode!(json) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/jsonb.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.JSONB do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | 5 | def init(opts) do 6 | json = 7 | Keyword.get_lazy(opts, :json, fn -> 8 | Application.get_env(:postgrex, :json_library, Jason) 9 | end) 10 | 11 | {json, Keyword.get(opts, :decode_binary, :copy)} 12 | end 13 | 14 | def matching({nil, _}), 15 | do: [] 16 | 17 | def matching(_), 18 | do: [type: "jsonb"] 19 | 20 | def format(_), 21 | do: :binary 22 | 23 | def encode({library, _}) do 24 | quote location: :keep do 25 | map -> 26 | data = unquote(library).encode_to_iodata!(map) 27 | [<> | data] 28 | end 29 | end 30 | 31 | def decode({library, :copy}) do 32 | quote location: :keep do 33 | <> -> 34 | <<1, json::binary>> = data 35 | 36 | json 37 | |> :binary.copy() 38 | |> unquote(library).decode!() 39 | end 40 | end 41 | 42 | def decode({library, :reference}) do 43 | quote location: :keep do 44 | <> -> 45 | <<1, json::binary>> = data 46 | unquote(library).decode!(json) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/line.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Line do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "line_send" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | %Postgrex.Line{a: a, b: b, c: c} when is_number(a) and is_number(b) and is_number(c) -> 9 | # a, b, c are 8 bytes each 10 | <<24::int32(), a::float64(), b::float64(), c::float64()>> 11 | 12 | other -> 13 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Line) 14 | end 15 | end 16 | 17 | def decode(_) do 18 | quote location: :keep do 19 | # a, b, c are 8 bytes each 20 | <<24::int32(), a::float64(), b::float64(), c::float64()>> -> 21 | %Postgrex.Line{a: a, b: b, c: c} 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/line_segment.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.LineSegment do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "lseg_send" 5 | alias Postgrex.Extensions.Point 6 | 7 | def encode(_) do 8 | quote location: :keep, generated: true do 9 | %Postgrex.LineSegment{point1: p1, point2: p2} -> 10 | encoded_p1 = Point.encode_point(p1, Postgrex.LineSegment) 11 | encoded_p2 = Point.encode_point(p2, Postgrex.LineSegment) 12 | # 2 points -> 16 bytes each 13 | [<<32::int32()>>, encoded_p1 | encoded_p2] 14 | 15 | other -> 16 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.LineSegment) 17 | end 18 | end 19 | 20 | def decode(_) do 21 | quote location: :keep do 22 | # 2 points -> 16 bytes each 23 | <<32::int32(), x1::float64(), y1::float64(), x2::float64(), y2::float64()>> -> 24 | p1 = %Postgrex.Point{x: x1, y: y1} 25 | p2 = %Postgrex.Point{x: x2, y: y2} 26 | %Postgrex.LineSegment{point1: p1, point2: p2} 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/lquery.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Lquery do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "lquery_send" 5 | 6 | def init(opts), do: Keyword.fetch!(opts, :decode_binary) 7 | 8 | def encode(_state) do 9 | quote location: :keep, generated: true do 10 | bin when is_binary(bin) -> 11 | # lquery binary formats are versioned 12 | # see: https://github.com/postgres/postgres/blob/master/contrib/ltree/ltree_io.c 13 | version = 1 14 | size = byte_size(bin) + 1 15 | [<> | bin] 16 | end 17 | end 18 | 19 | def decode(:reference) do 20 | quote location: :keep do 21 | <> -> 22 | <<_version::int8(), lquery::binary>> = bin 23 | lquery 24 | end 25 | end 26 | 27 | def decode(:copy) do 28 | quote location: :keep do 29 | <> -> 30 | <<_version::int8(), lquery::binary>> = bin 31 | :binary.copy(lquery) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/ltree.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Ltree do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "ltree_send" 5 | 6 | def init(opts), do: Keyword.fetch!(opts, :decode_binary) 7 | 8 | def encode(_state) do 9 | quote location: :keep, generated: true do 10 | bin when is_binary(bin) -> 11 | # ltree binary formats are versioned 12 | # see: https://github.com/postgres/postgres/blob/master/contrib/ltree/ltree_io.c 13 | version = 1 14 | size = byte_size(bin) + 1 15 | [<> | bin] 16 | end 17 | end 18 | 19 | def decode(:reference) do 20 | quote location: :keep do 21 | <> -> 22 | <<_version::int8(), ltree::binary>> = bin 23 | ltree 24 | end 25 | end 26 | 27 | def decode(:copy) do 28 | quote location: :keep do 29 | <> -> 30 | <<_version::int8(), ltree::binary>> = bin 31 | :binary.copy(ltree) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/ltxtquery.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Ltxtquery do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, type: "ltxtquery" 5 | 6 | @impl true 7 | def init(opts), do: Keyword.get(opts, :decode_binary, :copy) 8 | 9 | # ltxtquery binary formats are versioned 10 | # https://github.com/postgres/postgres/blob/master/contrib/ltree/ltxtquery_io.c 11 | @impl true 12 | def encode(_state) do 13 | quote location: :keep, generated: true do 14 | bin when is_binary(bin) -> 15 | version = 1 16 | size = byte_size(bin) + 1 17 | [<> | bin] 18 | end 19 | end 20 | 21 | @impl true 22 | def decode(:reference) do 23 | quote location: :keep do 24 | <> -> 25 | <<_version::int8(), ltxtquery::binary>> = bin 26 | ltxtquery 27 | end 28 | end 29 | 30 | def decode(:copy) do 31 | quote location: :keep do 32 | <> -> 33 | <<_version::int8(), ltxtquery::binary>> = bin 34 | :binary.copy(ltxtquery) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/macaddr.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.MACADDR do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "macaddr_send" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | %Postgrex.MACADDR{address: {a, b, c, d, e, f}} -> 9 | <<6::int32(), a, b, c, d, e, f>> 10 | 11 | other -> 12 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.MACADDR) 13 | end 14 | end 15 | 16 | def decode(_) do 17 | quote location: :keep do 18 | <<6::int32(), a::8, b::8, c::8, d::8, e::8, f::8>> -> 19 | %Postgrex.MACADDR{address: {a, b, c, d, e, f}} 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/multirange.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Multirange do 2 | @moduledoc false 3 | 4 | import Postgrex.BinaryUtils, warn: false 5 | 6 | @behaviour Postgrex.SuperExtension 7 | 8 | def init(_), do: nil 9 | 10 | def matching(_state), do: [send: "multirange_send"] 11 | 12 | def format(_), do: :super_binary 13 | 14 | def oids(%Postgrex.TypeInfo{base_type: base_oid}, _) do 15 | [base_oid] 16 | end 17 | 18 | def encode(_) do 19 | quote location: :keep do 20 | %Postgrex.Multirange{ranges: ranges}, [_oid], [type] when is_list(ranges) -> 21 | # encode_value/2 defined by TypeModule 22 | bound_encoder = &encode_value(&1, type) 23 | unquote(__MODULE__).encode(ranges, bound_encoder) 24 | 25 | other, _, _ -> 26 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Multirange) 27 | end 28 | end 29 | 30 | def decode(_) do 31 | quote location: :keep do 32 | <>, [_oid], [type] -> 33 | <<_num_ranges::int32(), ranges::binary>> = data 34 | 35 | # decode_list/2 defined by TypeModule 36 | sub_type_with_mod = 37 | case type do 38 | {extension, sub_oids, sub_types} -> {extension, sub_oids, sub_types, nil} 39 | extension -> {extension, nil} 40 | end 41 | 42 | bound_decoder = &decode_list(&1, sub_type_with_mod) 43 | unquote(__MODULE__).decode(ranges, bound_decoder, []) 44 | end 45 | end 46 | 47 | ## Helpers 48 | 49 | def encode(ranges, bound_encoder) do 50 | encoded_ranges = 51 | Enum.map(ranges, fn range -> 52 | %{lower: lower, upper: upper} = range 53 | lower = if is_atom(lower), do: lower, else: bound_encoder.(lower) 54 | upper = if is_atom(upper), do: upper, else: bound_encoder.(upper) 55 | Postgrex.Extensions.Range.encode(range, lower, upper) 56 | end) 57 | 58 | num_ranges = length(ranges) 59 | iodata = [<> | encoded_ranges] 60 | [<> | iodata] 61 | end 62 | 63 | def decode(<<>>, _bound_decoder, acc), do: %Postgrex.Multirange{ranges: Enum.reverse(acc)} 64 | 65 | def decode(<>, bound_decoder, acc) do 66 | <> = encoded_range 67 | 68 | decoded_range = 69 | case bound_decoder.(data) do 70 | [upper, lower] -> 71 | Postgrex.Extensions.Range.decode(flags, [lower, upper]) 72 | 73 | empty_or_one -> 74 | Postgrex.Extensions.Range.decode(flags, empty_or_one) 75 | end 76 | 77 | decode(rest, bound_decoder, [decoded_range | acc]) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/name.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Name do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "namesend" 5 | 6 | def init(opts), do: Keyword.fetch!(opts, :decode_binary) 7 | 8 | def encode(_) do 9 | quote location: :keep, generated: true do 10 | name when is_binary(name) and byte_size(name) < 64 -> 11 | [<> | name] 12 | 13 | other -> 14 | msg = "a binary string of less than 64 bytes" 15 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, msg) 16 | end 17 | end 18 | 19 | def decode(:reference) do 20 | quote location: :keep do 21 | <> -> name 22 | end 23 | end 24 | 25 | def decode(:copy) do 26 | quote location: :keep do 27 | <> -> :binary.copy(name) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/numeric.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Numeric do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "numeric_send" 5 | 6 | def encode(_) do 7 | quote location: :keep, generated: true do 8 | %Decimal{} = decimal -> 9 | data = unquote(__MODULE__).encode_numeric(decimal) 10 | [<> | data] 11 | 12 | n when is_float(n) -> 13 | data = unquote(__MODULE__).encode_numeric(Decimal.from_float(n)) 14 | [<> | data] 15 | 16 | n when is_integer(n) -> 17 | data = unquote(__MODULE__).encode_numeric(Decimal.new(n)) 18 | [<> | data] 19 | end 20 | end 21 | 22 | def decode(_) do 23 | quote location: :keep do 24 | <> -> 25 | unquote(__MODULE__).decode_numeric(data) 26 | end 27 | end 28 | 29 | ## Helpers 30 | 31 | # TODO: remove qNaN and sNaN when we depend on Decimal 2.0 32 | def encode_numeric(%Decimal{coef: coef}) when coef in [:NaN, :qNaN, :sNaN] do 33 | <<0::int16(), 0::int16(), 0xC000::uint16(), 0::int16()>> 34 | end 35 | 36 | def encode_numeric(%Decimal{sign: 1, coef: :inf}) do 37 | <<0::int16(), 0::int16(), 0xD000::uint16(), 0::int16()>> 38 | end 39 | 40 | def encode_numeric(%Decimal{sign: -1, coef: :inf}) do 41 | <<0::int16(), 0::int16(), 0xF000::uint16(), 0::int16()>> 42 | end 43 | 44 | def encode_numeric(%Decimal{sign: sign, coef: coef, exp: exp}) do 45 | sign = encode_sign(sign) 46 | scale = -exp 47 | 48 | {integer, float, scale} = split_parts(coef, scale) 49 | integer_digits = encode_digits(integer, []) 50 | float_digits = encode_float(float, scale) 51 | digits = integer_digits ++ float_digits 52 | 53 | num_digits = length(digits) 54 | weight = max(length(integer_digits) - 1, 0) 55 | 56 | bin = for digit <- digits, into: "", do: <> 57 | [<> | bin] 58 | end 59 | 60 | defp encode_sign(1), do: 0x0000 61 | defp encode_sign(-1), do: 0x4000 62 | 63 | defp split_parts(coef, scale) when scale >= 0 do 64 | integer_base = pow10(scale) 65 | {div(coef, integer_base), rem(coef, integer_base), scale} 66 | end 67 | 68 | defp split_parts(coef, scale) when scale < 0 do 69 | integer_base = pow10(-scale) 70 | {coef * integer_base, 0, 0} 71 | end 72 | 73 | defp encode_float(float, scale) do 74 | pending = pending_scale(float, scale) 75 | float_prefix = div(pending, 4) 76 | float_suffix = 4 - rem(scale, 4) 77 | float = float * pow10(float_suffix) 78 | List.duplicate(0, float_prefix) ++ encode_digits(float, []) 79 | end 80 | 81 | defp pending_scale(0, scale), do: scale 82 | defp pending_scale(num, scale), do: pending_scale(div(num, 10), scale - 1) 83 | 84 | defp encode_digits(coef, digits) when coef < 10_000 do 85 | [coef | digits] 86 | end 87 | 88 | defp encode_digits(coef, digits) do 89 | digit = rem(coef, 10_000) 90 | coef = div(coef, 10_000) 91 | encode_digits(coef, [digit | digits]) 92 | end 93 | 94 | def decode_numeric( 95 | <> 96 | ) do 97 | decode_numeric(ndigits, weight, sign, scale, tail) 98 | end 99 | 100 | @nan Decimal.new("NaN") 101 | @positive_inf Decimal.new("Inf") 102 | @negative_inf Decimal.new("-Inf") 103 | 104 | defp decode_numeric(0, _weight, 0xC000, _scale, "") do 105 | @nan 106 | end 107 | 108 | defp decode_numeric(0, _weight, 0xD000, _scale, "") do 109 | @positive_inf 110 | end 111 | 112 | defp decode_numeric(0, _weight, 0xF000, _scale, "") do 113 | @negative_inf 114 | end 115 | 116 | defp decode_numeric(_num_digits, weight, sign, scale, bin) do 117 | {value, weight} = decode_numeric_int(bin, weight, 0) 118 | sign = decode_sign(sign) 119 | coef = scale(value, (weight + 1) * 4 + scale) 120 | Decimal.new(sign, coef, -scale) 121 | end 122 | 123 | defp decode_sign(0x0000), do: 1 124 | defp decode_sign(0x4000), do: -1 125 | 126 | defp scale(coef, 0), do: coef 127 | defp scale(coef, diff) when diff < 0, do: div(coef, pow10(-diff)) 128 | defp scale(coef, diff) when diff > 0, do: coef * pow10(diff) 129 | 130 | Enum.reduce(0..100, 1, fn x, acc -> 131 | defp pow10(unquote(x)), do: unquote(acc) 132 | acc * 10 133 | end) 134 | 135 | defp pow10(num) when num > 100, do: pow10(100) * pow10(num - 100) 136 | 137 | defp decode_numeric_int("", weight, acc), do: {acc, weight} 138 | 139 | defp decode_numeric_int(<>, weight, acc) do 140 | acc = acc * 10_000 + digit 141 | decode_numeric_int(tail, weight - 1, acc) 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/oid.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.OID do 2 | @moduledoc false 3 | @oid_senders ~w(oidsend regprocsend regproceduresend regopersend 4 | regoperatorsend regclasssend regtypesend regconfigsend xidsend cidsend) 5 | 6 | import Postgrex.BinaryUtils, warn: false 7 | use Postgrex.BinaryExtension, Enum.map(@oid_senders, &{:send, &1}) 8 | 9 | @oid_range 0..4_294_967_295 10 | 11 | def encode(_) do 12 | range = Macro.escape(@oid_range) 13 | 14 | quote location: :keep do 15 | oid when is_integer(oid) and oid in unquote(range) -> 16 | <<4::int32(), oid::uint32()>> 17 | 18 | binary when is_binary(binary) -> 19 | msg = 20 | "you tried to use a binary for an oid type " <> 21 | "(#{binary}) when an integer was expected. See " <> 22 | "https://github.com/elixir-ecto/postgrex#oid-type-encoding" 23 | 24 | raise ArgumentError, msg 25 | 26 | other -> 27 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, unquote(range)) 28 | end 29 | end 30 | 31 | def decode(_) do 32 | quote location: :keep do 33 | <<4::int32(), oid::uint32()>> -> oid 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/path.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Path do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "path_send" 5 | alias Postgrex.Extensions.Path 6 | alias Postgrex.Extensions.Point 7 | 8 | def encode(_) do 9 | quote location: :keep, generated: true do 10 | %Postgrex.Path{open: o, points: ps} when is_list(ps) and is_boolean(o) -> 11 | open_byte = Path.open_to_byte(o) 12 | len = length(ps) 13 | 14 | encoded_points = 15 | Enum.reduce(ps, [], fn p, acc -> [acc | Point.encode_point(p, Postgrex.Path)] end) 16 | 17 | # 1 byte for open/closed flag, 4 for length, 16 for each point 18 | nbytes = 5 + 16 * len 19 | [<>, open_byte, <> | encoded_points] 20 | 21 | other -> 22 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Path) 23 | end 24 | end 25 | 26 | def decode(_) do 27 | quote location: :keep do 28 | <> -> 29 | Path.decode_path(path_data) 30 | end 31 | end 32 | 33 | def decode_path(<>) do 34 | open = o == 0 35 | points = decode_points(point_data, []) 36 | %Postgrex.Path{open: open, points: points} 37 | end 38 | 39 | def open_to_byte(true), do: 0 40 | def open_to_byte(false), do: 1 41 | 42 | defp decode_points(<<>>, points), do: Enum.reverse(points) 43 | 44 | defp decode_points(<>, points) do 45 | decode_points(rest, [%Postgrex.Point{x: x, y: y} | points]) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/point.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Point do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "point_send" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | %Postgrex.Point{x: x, y: y} -> 9 | <<16::int32(), x::float64(), y::float64()>> 10 | 11 | other -> 12 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Point) 13 | end 14 | end 15 | 16 | def decode(_) do 17 | quote location: :keep do 18 | <<16::int32(), x::float64(), y::float64()>> -> %Postgrex.Point{x: x, y: y} 19 | end 20 | end 21 | 22 | # used by other extensions 23 | def encode_point(%Postgrex.Point{x: x, y: y}, _) do 24 | <> 25 | end 26 | 27 | def encode_point(other, wanted) do 28 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, wanted) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/polygon.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Polygon do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "poly_send" 5 | alias Postgrex.Extensions.Polygon 6 | alias Postgrex.Extensions.Point 7 | 8 | def encode(_) do 9 | quote location: :keep, generated: true do 10 | %Postgrex.Polygon{vertices: vertices} when is_list(vertices) -> 11 | len = length(vertices) 12 | 13 | vert = 14 | Enum.reduce(vertices, [], fn v, acc -> 15 | [acc | Point.encode_point(v, Postgrex.Polygon)] 16 | end) 17 | 18 | # 32 bits for len, 64 for each x and each y 19 | nbytes = 4 + 16 * len 20 | 21 | [<>, <> | vert] 22 | 23 | other -> 24 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Polygon) 25 | end 26 | end 27 | 28 | def decode(_) do 29 | quote location: :keep do 30 | <> -> 31 | vertices = Polygon.decode_vertices(polygon_data) 32 | %Postgrex.Polygon{vertices: vertices} 33 | end 34 | end 35 | 36 | # n vertices, 128 bits for each vertex - 64 for x, 64 for y 37 | def decode_vertices(<>) do 38 | decode_vertices(vert_data, []) 39 | end 40 | 41 | defp decode_vertices(<<>>, v), do: Enum.reverse(v) 42 | 43 | defp decode_vertices(<>, v) do 44 | decode_vertices(rest, [%Postgrex.Point{x: x, y: y} | v]) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/range.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Range do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | import Bitwise 5 | @behaviour Postgrex.SuperExtension 6 | 7 | @range_empty 0x01 8 | @range_lb_inc 0x02 9 | @range_ub_inc 0x04 10 | @range_lb_inf 0x08 11 | @range_ub_inf 0x10 12 | 13 | def init(_), do: nil 14 | 15 | def matching(_), do: [send: "range_send"] 16 | 17 | def format(_), do: :super_binary 18 | 19 | def oids(%Postgrex.TypeInfo{base_type: base_oid}, _) do 20 | [base_oid] 21 | end 22 | 23 | def encode(_) do 24 | quote location: :keep do 25 | %Postgrex.Range{lower: lower, upper: upper} = range, [_oid], [type] -> 26 | # encode_value/2 defined by TypeModule 27 | lower = if is_atom(lower), do: lower, else: encode_value(lower, type) 28 | upper = if is_atom(upper), do: upper, else: encode_value(upper, type) 29 | unquote(__MODULE__).encode(range, lower, upper) 30 | 31 | other, _, _ -> 32 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Range) 33 | end 34 | end 35 | 36 | def decode(_) do 37 | quote location: :keep do 38 | <>, [_oid], [type] -> 39 | <> = binary 40 | 41 | # decode_list/2 defined by TypeModule 42 | sub_type_with_mod = 43 | case type do 44 | {extension, sub_oids, sub_types} -> {extension, sub_oids, sub_types, nil} 45 | extension -> {extension, nil} 46 | end 47 | 48 | case decode_list(data, sub_type_with_mod) do 49 | [upper, lower] -> 50 | unquote(__MODULE__).decode(flags, [lower, upper]) 51 | 52 | empty_or_one -> 53 | unquote(__MODULE__).decode(flags, empty_or_one) 54 | end 55 | end 56 | end 57 | 58 | ## Helpers 59 | 60 | def encode(_range, :empty, :empty) do 61 | [<<1::int32(), @range_empty>>] 62 | end 63 | 64 | def encode(%{lower_inclusive: lower_inc, upper_inclusive: upper_inc}, lower, upper) do 65 | flags = 0 66 | 67 | {flags, data} = 68 | if is_atom(lower) do 69 | {flags ||| @range_lb_inf, []} 70 | else 71 | {flags, lower} 72 | end 73 | 74 | {flags, data} = 75 | if is_atom(upper) do 76 | {flags ||| @range_ub_inf, data} 77 | else 78 | {flags, [data | upper]} 79 | end 80 | 81 | flags = 82 | case lower_inc do 83 | true -> flags ||| @range_lb_inc 84 | false -> flags 85 | end 86 | 87 | flags = 88 | case upper_inc do 89 | true -> flags ||| @range_ub_inc 90 | false -> flags 91 | end 92 | 93 | [<>, flags | data] 94 | end 95 | 96 | def decode(flags, []) when (flags &&& @range_empty) != 0 do 97 | %Postgrex.Range{ 98 | lower: :empty, 99 | upper: :empty, 100 | lower_inclusive: false, 101 | upper_inclusive: false 102 | } 103 | end 104 | 105 | def decode(flags, elems) do 106 | {lower, elems} = 107 | if (flags &&& @range_lb_inf) != 0 do 108 | {:unbound, elems} 109 | else 110 | [lower | rest] = elems 111 | {lower, rest} 112 | end 113 | 114 | {upper, []} = 115 | if (flags &&& @range_ub_inf) != 0 do 116 | {:unbound, elems} 117 | else 118 | [upper | rest] = elems 119 | {upper, rest} 120 | end 121 | 122 | lower_inclusive? = (flags &&& @range_lb_inc) != 0 123 | upper_inclusive? = (flags &&& @range_ub_inc) != 0 124 | 125 | %Postgrex.Range{ 126 | lower: lower, 127 | upper: upper, 128 | lower_inclusive: lower_inclusive?, 129 | upper_inclusive: upper_inclusive? 130 | } 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/raw.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Raw do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | 5 | use Postgrex.BinaryExtension, 6 | send: "bpcharsend", 7 | send: "textsend", 8 | send: "varcharsend", 9 | send: "byteasend", 10 | send: "enum_send", 11 | send: "unknownsend", 12 | send: "citextsend", 13 | send: "charsend" 14 | 15 | def init(opts), do: Keyword.fetch!(opts, :decode_binary) 16 | 17 | def encode(_) do 18 | quote location: :keep, generated: true do 19 | bin when is_binary(bin) -> 20 | [<> | bin] 21 | 22 | other -> 23 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a binary") 24 | end 25 | end 26 | 27 | def decode(:copy) do 28 | quote location: :keep do 29 | <> -> :binary.copy(value) 30 | end 31 | end 32 | 33 | def decode(:reference) do 34 | quote location: :keep do 35 | <> -> value 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/record.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Record do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | @behaviour Postgrex.SuperExtension 5 | 6 | def init(_), do: nil 7 | 8 | def matching(_), 9 | do: [send: "record_send"] 10 | 11 | def format(_), 12 | do: :super_binary 13 | 14 | def oids(%Postgrex.TypeInfo{comp_elems: []}, _), 15 | do: nil 16 | 17 | def oids(%Postgrex.TypeInfo{comp_elems: comp_oids}, _), 18 | do: comp_oids 19 | 20 | def encode(_) do 21 | quote location: :keep do 22 | tuple, oids, types when is_tuple(tuple) -> 23 | # encode_tuple/3 defined by TypeModule 24 | data = encode_tuple(tuple, oids, types) 25 | [<> | data] 26 | 27 | other, _, _ -> 28 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a tuple") 29 | end 30 | end 31 | 32 | def decode(_) do 33 | quote location: :keep do 34 | <>, nil, types -> 35 | <> = binary 36 | # decode_tuple/3 defined by TypeModule 37 | decode_tuple(data, count, types) 38 | 39 | <>, oids, types -> 40 | <<_::int32(), data::binary>> = binary 41 | # decode_tuple/3 defined by TypeModule 42 | decode_tuple(data, oids, types) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/tid.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.TID do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "tidsend" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | {block, tuple} -> 9 | <<6::int32(), block::uint32(), tuple::uint16()>> 10 | 11 | other -> 12 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a tuple of 2 integers") 13 | end 14 | end 15 | 16 | def decode(_) do 17 | quote location: :keep do 18 | <<6::int32(), block::uint32(), tuple::uint16()>> -> 19 | {block, tuple} 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/time.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Time do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "time_send" 5 | 6 | @default_precision 6 7 | # -1: user did not specify precision 8 | # nil: coming from a super type that does not pass modifier for sub-type 9 | @unspecified_precision [-1, nil] 10 | 11 | def encode(_) do 12 | quote location: :keep do 13 | %Time{calendar: Calendar.ISO} = time -> 14 | unquote(__MODULE__).encode_elixir(time) 15 | 16 | other -> 17 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Time) 18 | end 19 | end 20 | 21 | def decode(_) do 22 | quote location: :keep do 23 | <<8::int32(), microsecs::int64()>> -> 24 | unquote(__MODULE__).microsecond_to_elixir(microsecs, var!(mod)) 25 | end 26 | end 27 | 28 | ## Helpers 29 | 30 | def encode_elixir(%Time{hour: hour, minute: min, second: sec, microsecond: {usec, _}}) 31 | when hour in 0..23 and min in 0..59 and sec in 0..59 and usec in 0..999_999 do 32 | time = {hour, min, sec} 33 | <<8::int32(), :calendar.time_to_seconds(time) * 1_000_000 + usec::int64()>> 34 | end 35 | 36 | def microsecond_to_elixir(microsec, precision) do 37 | precision = if precision in @unspecified_precision, do: @default_precision, else: precision 38 | sec = div(microsec, 1_000_000) 39 | microsec = rem(microsec, 1_000_000) 40 | 41 | sec 42 | |> :calendar.seconds_to_time() 43 | |> Time.from_erl!({microsec, precision}) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/timestamp.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Timestamp do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "timestamp_send" 5 | 6 | @gs_epoch NaiveDateTime.to_gregorian_seconds(~N[2000-01-01 00:00:00]) |> elem(0) 7 | @max_year 294_276 8 | @min_year -4_713 9 | @plus_infinity 9_223_372_036_854_775_807 10 | @minus_infinity -9_223_372_036_854_775_808 11 | @default_precision 6 12 | # -1: user did not specify precision 13 | # nil: coming from a super type that does not pass modifier for sub-type 14 | @unspecified_precision [-1, nil] 15 | 16 | def init(opts), do: Keyword.get(opts, :allow_infinite_timestamps, false) 17 | 18 | def encode(_) do 19 | quote location: :keep do 20 | %NaiveDateTime{calendar: Calendar.ISO} = naive -> 21 | unquote(__MODULE__).encode_elixir(naive) 22 | 23 | %DateTime{calendar: Calendar.ISO} = dt -> 24 | unquote(__MODULE__).encode_elixir(dt) 25 | 26 | other -> 27 | raise DBConnection.EncodeError, 28 | Postgrex.Utils.encode_msg(other, {DateTime, NaiveDateTime}) 29 | end 30 | end 31 | 32 | def decode(infinity?) do 33 | quote location: :keep do 34 | <<8::int32(), microsecs::int64()>> -> 35 | unquote(__MODULE__).microsecond_to_elixir(microsecs, var!(mod), unquote(infinity?)) 36 | end 37 | end 38 | 39 | ## Helpers 40 | 41 | def encode_elixir( 42 | %_{ 43 | year: year, 44 | hour: hour, 45 | minute: min, 46 | second: sec, 47 | microsecond: {usec, _} 48 | } = date_time 49 | ) 50 | when year <= @max_year and year >= @min_year and hour in 0..23 and min in 0..59 and 51 | sec in 0..59 and 52 | usec in 0..999_999 do 53 | {gregorian_seconds, usec} = NaiveDateTime.to_gregorian_seconds(date_time) 54 | secs = gregorian_seconds - @gs_epoch 55 | <<8::int32(), secs * 1_000_000 + usec::int64()>> 56 | end 57 | 58 | def microsecond_to_elixir(@plus_infinity, _precision, infinity?) do 59 | if infinity?, do: :inf, else: raise_infinity("infinity") 60 | end 61 | 62 | def microsecond_to_elixir(@minus_infinity, _precision, infinity?) do 63 | if infinity?, do: :"-inf", else: raise_infinity("-infinity") 64 | end 65 | 66 | def microsecond_to_elixir(microsecs, precision, _infinity) do 67 | split(microsecs, precision) 68 | end 69 | 70 | defp split(microsecs, precision) when microsecs < 0 and rem(microsecs, 1_000_000) != 0 do 71 | secs = div(microsecs, 1_000_000) - 1 72 | microsecs = 1_000_000 + rem(microsecs, 1_000_000) 73 | split(secs, microsecs, precision) 74 | end 75 | 76 | defp split(microsecs, precision) do 77 | secs = div(microsecs, 1_000_000) 78 | microsecs = rem(microsecs, 1_000_000) 79 | split(secs, microsecs, precision) 80 | end 81 | 82 | defp split(secs, microsecs, precision) do 83 | precision = if precision in @unspecified_precision, do: @default_precision, else: precision 84 | NaiveDateTime.from_gregorian_seconds(secs + @gs_epoch, {microsecs, precision}) 85 | end 86 | 87 | defp raise_infinity(type) do 88 | raise ArgumentError, """ 89 | got \"#{type}\" from PostgreSQL. If you want to support infinity timestamps \ 90 | in your application, you can enable them by defining your own types: 91 | 92 | Postgrex.Types.define(MyApp.PostgrexTypes, [], allow_infinite_timestamps: true) 93 | 94 | And then configuring your database to use it: 95 | 96 | types: MyApp.PostgrexTypes 97 | """ 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/timestamptz.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.TimestampTZ do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "timestamptz_send" 5 | 6 | @gs_epoch NaiveDateTime.to_gregorian_seconds(~N[2000-01-01 00:00:00.0]) |> elem(0) 7 | @gs_unix_epoch NaiveDateTime.to_gregorian_seconds(~N[1970-01-01 00:00:00.0]) |> elem(0) 8 | @us_epoch (@gs_epoch - @gs_unix_epoch) * 1_000_000 9 | 10 | @plus_infinity 9_223_372_036_854_775_807 11 | @minus_infinity -9_223_372_036_854_775_808 12 | @default_precision 6 13 | # -1: user did not specify precision 14 | # nil: coming from a super type that does not pass modifier for sub-type 15 | @unspecified_precision [-1, nil] 16 | 17 | def init(opts), do: Keyword.get(opts, :allow_infinite_timestamps, false) 18 | 19 | def encode(_) do 20 | quote location: :keep do 21 | %DateTime{calendar: Calendar.ISO} = dt -> 22 | unquote(__MODULE__).encode_elixir(dt) 23 | 24 | other -> 25 | raise DBConnection.EncodeError, 26 | Postgrex.Utils.encode_msg(other, DateTime) 27 | end 28 | end 29 | 30 | def decode(infinity?) do 31 | quote location: :keep do 32 | <<8::int32(), microsecs::int64()>> -> 33 | unquote(__MODULE__).microsecond_to_elixir(microsecs, var!(mod), unquote(infinity?)) 34 | end 35 | end 36 | 37 | ## Helpers 38 | 39 | def encode_elixir(%DateTime{utc_offset: 0, std_offset: 0} = datetime) do 40 | microsecs = DateTime.to_unix(datetime, :microsecond) 41 | <<8::int32(), microsecs - @us_epoch::int64()>> 42 | end 43 | 44 | def encode_elixir(%DateTime{} = datetime) do 45 | raise ArgumentError, "#{inspect(datetime)} is not in UTC" 46 | end 47 | 48 | def microsecond_to_elixir(@plus_infinity, _precision, infinity?) do 49 | if infinity?, do: :inf, else: raise_infinity("infinity") 50 | end 51 | 52 | def microsecond_to_elixir(@minus_infinity, _precision, infinity?) do 53 | if infinity?, do: :"-inf", else: raise_infinity("-infinity") 54 | end 55 | 56 | def microsecond_to_elixir(microsecs, precision, _infinity) do 57 | split(microsecs, precision) 58 | end 59 | 60 | defp split(microsecs, precision) when microsecs < 0 and rem(microsecs, 1_000_000) != 0 do 61 | secs = div(microsecs, 1_000_000) - 1 62 | microsecs = 1_000_000 + rem(microsecs, 1_000_000) 63 | split(secs, microsecs, precision) 64 | end 65 | 66 | defp split(microsecs, precision) do 67 | secs = div(microsecs, 1_000_000) 68 | microsecs = rem(microsecs, 1_000_000) 69 | split(secs, microsecs, precision) 70 | end 71 | 72 | defp split(secs, microsecs, precision) do 73 | precision = if precision in @unspecified_precision, do: @default_precision, else: precision 74 | DateTime.from_gregorian_seconds(secs + @gs_epoch, {microsecs, precision}) 75 | end 76 | 77 | defp raise_infinity(type) do 78 | raise ArgumentError, """ 79 | got \"#{type}\" from PostgreSQL. If you want to support infinity timestamps \ 80 | in your application, you can enable them by defining your own types: 81 | 82 | Postgrex.Types.define(MyApp.PostgrexTypes, [], allow_infinite_timestamps: true) 83 | 84 | And then configuring your database to use it: 85 | 86 | types: MyApp.PostgrexTypes 87 | """ 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/timetz.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.TimeTZ do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "timetz_send" 5 | 6 | @day (:calendar.time_to_seconds({23, 59, 59}) + 1) * 1_000_000 7 | @default_precision 6 8 | # -1: user did not specify precision 9 | # nil: coming from a super type that does not pass modifier for sub-type 10 | @unspecified_precision [-1, nil] 11 | 12 | def encode(_) do 13 | quote location: :keep do 14 | %Time{calendar: Calendar.ISO} = time -> 15 | unquote(__MODULE__).encode_elixir(time) 16 | 17 | other -> 18 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Time) 19 | end 20 | end 21 | 22 | def decode(_) do 23 | quote location: :keep do 24 | <<12::int32(), microsecs::int64(), tz::int32()>> -> 25 | unquote(__MODULE__).microsecond_to_elixir(microsecs, var!(mod), tz) 26 | end 27 | end 28 | 29 | ## Helpers 30 | 31 | defp adjust_microsecond(microsec, tz) do 32 | case microsec + tz * 1_000_000 do 33 | adjusted_microsec when adjusted_microsec < 0 -> 34 | @day + adjusted_microsec 35 | 36 | adjusted_microsec when adjusted_microsec < @day -> 37 | adjusted_microsec 38 | 39 | adjusted_microsec -> 40 | adjusted_microsec - @day 41 | end 42 | end 43 | 44 | def encode_elixir(%Time{hour: hour, minute: min, second: sec, microsecond: {usec, _}}) 45 | when hour in 0..23 and min in 0..59 and sec in 0..59 and usec in 0..999_999 do 46 | time = {hour, min, sec} 47 | <<12::int32(), :calendar.time_to_seconds(time) * 1_000_000 + usec::int64(), 0::int32()>> 48 | end 49 | 50 | def microsecond_to_elixir(microsec, precision, tz) do 51 | microsec 52 | |> adjust_microsecond(tz) 53 | |> microsecond_to_elixir(precision) 54 | end 55 | 56 | defp microsecond_to_elixir(microsec, precision) do 57 | precision = if precision in @unspecified_precision, do: @default_precision, else: precision 58 | sec = div(microsec, 1_000_000) 59 | microsec = rem(microsec, 1_000_000) 60 | 61 | sec 62 | |> :calendar.seconds_to_time() 63 | |> Time.from_erl!({microsec, precision}) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/tsvector.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.TSVector do 2 | @moduledoc false 3 | 4 | import Postgrex.BinaryUtils, warn: false 5 | use Postgrex.BinaryExtension, send: "tsvectorsend" 6 | 7 | def encode(_) do 8 | quote location: :keep do 9 | values when is_list(values) -> 10 | encoded_tsvectors = unquote(__MODULE__).encode_tsvector(values) 11 | <> 12 | 13 | other -> 14 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a list of tsvectors") 15 | end 16 | end 17 | 18 | def decode(_) do 19 | quote do 20 | <> -> 21 | <> = value 22 | unquote(__MODULE__).decode_tsvector_values(words) 23 | end 24 | end 25 | 26 | ## Helpers 27 | 28 | def encode_tsvector(values) do 29 | <> 30 | end 31 | 32 | defp encode_lexemes(values) do 33 | values |> Enum.map(fn x -> encode_positions(x) end) |> IO.iodata_to_binary() 34 | end 35 | 36 | defp encode_positions(%Postgrex.Lexeme{word: word, positions: positions}) do 37 | positions = 38 | Enum.map(positions, fn {position, weight} -> 39 | <> 40 | end) 41 | 42 | [word, 0, <> | positions] 43 | end 44 | 45 | def decode_tsvector_values("") do 46 | [] 47 | end 48 | 49 | def decode_tsvector_values(words) do 50 | [word, <>] = :binary.split(words, <<0>>) 51 | positions_bytes = positions_count * 2 52 | <> = rest 53 | 54 | positions = 55 | for <>, do: {position, decode_weight(weight)} 56 | 57 | [%Postgrex.Lexeme{word: word, positions: positions} | decode_tsvector_values(remaining_data)] 58 | end 59 | 60 | defp encode_weight_binary(:A) do 61 | 3 62 | end 63 | 64 | defp encode_weight_binary(:B) do 65 | 2 66 | end 67 | 68 | defp encode_weight_binary(:C) do 69 | 1 70 | end 71 | 72 | defp encode_weight_binary(nil) do 73 | 0 74 | end 75 | 76 | defp decode_weight(0), do: nil 77 | defp decode_weight(1), do: :C 78 | defp decode_weight(2), do: :B 79 | defp decode_weight(3), do: :A 80 | end 81 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/uuid.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.UUID do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "uuid_send" 5 | 6 | def init(opts), do: Keyword.fetch!(opts, :decode_binary) 7 | 8 | def encode(_) do 9 | quote location: :keep, generated: true do 10 | uuid when is_binary(uuid) and byte_size(uuid) == 16 -> 11 | [<<16::int32()>> | uuid] 12 | 13 | other -> 14 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a binary of 16 bytes") 15 | end 16 | end 17 | 18 | def decode(:copy) do 19 | quote location: :keep do 20 | <<16::int32(), uuid::binary-16>> -> :binary.copy(uuid) 21 | end 22 | end 23 | 24 | def decode(:reference) do 25 | quote location: :keep do 26 | <<16::int32(), uuid::binary-16>> -> uuid 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/void_binary.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.VoidBinary do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "void_send" 5 | 6 | def encode(_) do 7 | quote location: :keep do 8 | :void -> 9 | <<0::int32()>> 10 | 11 | other -> 12 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "the atom :void") 13 | end 14 | end 15 | 16 | def decode(_) do 17 | quote location: :keep do 18 | <<0::int32()>> -> :void 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/void_text.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.VoidText do 2 | @moduledoc false 3 | @behaviour Postgrex.Extension 4 | import Postgrex.BinaryUtils, warn: false 5 | 6 | def init(_), do: nil 7 | 8 | def matching(_), do: [output: "void_out"] 9 | 10 | def format(_), do: :text 11 | 12 | def encode(_) do 13 | quote location: :keep do 14 | :void -> 15 | <<0::int32()>> 16 | 17 | other -> 18 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "the atom :void") 19 | end 20 | end 21 | 22 | def decode(_) do 23 | quote location: :keep do 24 | <<0::int32()>> -> :void 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/postgrex/extensions/xid8.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Extensions.Xid8 do 2 | @moduledoc false 3 | import Postgrex.BinaryUtils, warn: false 4 | use Postgrex.BinaryExtension, send: "xid8send" 5 | 6 | @xid8_range 0..18_446_744_073_709_551_615 7 | 8 | def encode(_) do 9 | range = Macro.escape(@xid8_range) 10 | 11 | quote location: :keep do 12 | int when int in unquote(range) -> 13 | <<8::int32(), int::uint64()>> 14 | 15 | other -> 16 | raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, unquote(range)) 17 | end 18 | end 19 | 20 | def decode(_) do 21 | quote location: :keep do 22 | <<8::int32(), int::uint64()>> -> int 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/postgrex/notifications.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Notifications do 2 | @moduledoc ~S""" 3 | API for notifications (pub/sub) in PostgreSQL. 4 | 5 | In order to use it, first you need to start the notification process. 6 | In your supervision tree: 7 | 8 | {Postgrex.Notifications, name: MyApp.Notifications} 9 | 10 | Then you can listen to certain channels: 11 | 12 | {:ok, listen_ref} = Postgrex.Notifications.listen(MyApp.Notifications, "channel") 13 | 14 | Now every time a message is broadcast on said channel, for example via 15 | PostgreSQL command line: 16 | 17 | NOTIFY "channel", "Oh hai!"; 18 | 19 | You will receive a message in the format: 20 | 21 | {:notification, notification_pid, listen_ref, channel, message} 22 | 23 | ## Async connect, auto-reconnects and missed notifications 24 | 25 | By default, the notification system establishes a connection to the 26 | database on initialization, you can configure the connection to happen 27 | asynchronously. You can also configure the connection to automatically 28 | reconnect. 29 | 30 | Note however that when the notification system is waiting for a connection, 31 | any notifications that occur during the disconnection period are not queued 32 | and cannot be recovered. Similarly, any listen command will be queued until 33 | the connection is up. 34 | 35 | There is a race condition between starting to listen and notifications being 36 | issued "at the same time", as explained [in the PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-listen.html). 37 | If your application needs to keep a consistent representation of data, follow 38 | the three-step approach of first subscribing, then obtaining the current 39 | state of data, then handling the incoming notifications. 40 | 41 | Beware that the same 42 | race condition applies to auto-reconnects. A simple way of dealing with this 43 | issue is not using the auto-reconnect feature directly, but monitoring and 44 | re-starting the Notifications process, then subscribing to channel messages 45 | over again, using the same three-step approach. 46 | 47 | ## A note on casing 48 | 49 | While PostgreSQL seems to behave as case-insensitive, it actually has a very 50 | peculiar behaviour on casing. When you write: 51 | 52 | SELECT * FROM POSTS 53 | 54 | PostgreSQL actually converts `POSTS` into the lowercase `posts`. That's why 55 | both `SELECT * FROM POSTS` and `SELECT * FROM posts` feel equivalent. 56 | However, if you wrap the table name in quotes, then the casing in quotes 57 | will be preserved. 58 | 59 | These same rules apply to PostgreSQL notification channels. More importantly, 60 | whenever `Postgrex.Notifications` listens to a channel, it wraps the channel 61 | name in quotes. Therefore, if you listen to a channel named "fooBar" and 62 | you send a notification without quotes in the channel name, such as: 63 | 64 | NOTIFY fooBar, "Oh hai!"; 65 | 66 | The notification will not be received by Postgrex.Notifications because the 67 | notification will be effectively sent to `"foobar"` and not `"fooBar"`. Therefore, 68 | you must guarantee one of the two following properties: 69 | 70 | 1. If you can wrap the channel name in quotes when sending a notification, 71 | then make sure the channel name has the exact same casing when listening 72 | and sending notifications 73 | 74 | 2. If you cannot wrap the channel name in quotes when sending a notification, 75 | then make sure to give the lowercased channel name when listening 76 | """ 77 | 78 | @typedoc since: "0.17.0" 79 | @type server :: :gen_statem.from() 80 | 81 | alias Postgrex.SimpleConnection 82 | 83 | @behaviour SimpleConnection 84 | 85 | require Logger 86 | 87 | defstruct [ 88 | :from, 89 | :ref, 90 | auto_reconnect: false, 91 | connected: false, 92 | listeners: %{}, 93 | listener_channels: %{} 94 | ] 95 | 96 | @timeout 5000 97 | 98 | @doc false 99 | def child_spec(opts) do 100 | %{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}} 101 | end 102 | 103 | @doc """ 104 | Start the notification connection process and connect to postgres. 105 | 106 | The options that this function accepts are the same as those accepted by 107 | `Postgrex.start_link/1`, as well as the extra options `:sync_connect`, 108 | `:auto_reconnect`, `:reconnect_backoff`, and `:configure`. 109 | 110 | ## Options 111 | 112 | * `:sync_connect` - controls if the connection should be established on boot 113 | or asynchronously right after boot. Defaults to `true`. 114 | 115 | * `:auto_reconnect` - automatically attempt to reconnect to the database 116 | in event of a disconnection. See the 117 | [note about async connect and auto-reconnects](#module-async-connect-and-auto-reconnects) 118 | above. Defaults to `false`, which means the process terminates. 119 | 120 | * `:reconnect_backoff` - time (in ms) between reconnection attempts when 121 | `auto_reconnect` is enabled. Defaults to `500`. 122 | 123 | * `:idle_interval` - while also accepted on `Postgrex.start_link/1`, it has 124 | a default of `5000ms` in `Postgrex.Notifications` (instead of 1000ms). 125 | 126 | * `:configure` - A function to run before every connect attempt to dynamically 127 | configure the options as a `{module, function, args}`, where the current 128 | options will prepended to `args`. Defaults to `nil`. 129 | """ 130 | @spec start_link(Keyword.t()) :: {:ok, pid} | {:error, Postgrex.Error.t() | term} 131 | def start_link(opts) do 132 | args = Keyword.take(opts, [:auto_reconnect]) 133 | 134 | SimpleConnection.start_link(__MODULE__, args, opts) 135 | end 136 | 137 | @doc """ 138 | Listens to an asynchronous notification channel using the `LISTEN` command. 139 | 140 | A message `{:notification, connection_pid, ref, channel, payload}` will be 141 | sent to the calling process when a notification is received. 142 | 143 | It returns `{:ok, reference}`. It may also return `{:eventually, reference}` 144 | if the notification process is not currently connected to the database and 145 | it was started with `:sync_connect` set to false or `:auto_reconnect` set 146 | to true. The `reference` can be used to issue an `unlisten/3` command. 147 | 148 | ## Options 149 | 150 | * `:timeout` - Call timeout (default: `#{@timeout}`) 151 | """ 152 | @spec listen(server, String.t(), Keyword.t()) :: 153 | {:ok, reference} | {:eventually, reference} 154 | def listen(pid, channel, opts \\ []) do 155 | SimpleConnection.call(pid, {:listen, channel}, Keyword.get(opts, :timeout, @timeout)) 156 | end 157 | 158 | @doc """ 159 | Listens to an asynchronous notification channel `channel`. See `listen/2`. 160 | """ 161 | @spec listen!(server, String.t(), Keyword.t()) :: reference 162 | def listen!(pid, channel, opts \\ []) do 163 | {:ok, ref} = listen(pid, channel, opts) 164 | ref 165 | end 166 | 167 | @doc """ 168 | Stops listening on the given channel by passing the reference returned from 169 | `listen/2`. 170 | 171 | ## Options 172 | 173 | * `:timeout` - Call timeout (default: `#{@timeout}`) 174 | """ 175 | @spec unlisten(server, reference, Keyword.t()) :: :ok | :error 176 | def unlisten(pid, ref, opts \\ []) do 177 | SimpleConnection.call(pid, {:unlisten, ref}, Keyword.get(opts, :timeout, @timeout)) 178 | end 179 | 180 | @doc """ 181 | Stops listening on the given channel by passing the reference returned from 182 | `listen/2`. 183 | """ 184 | @spec unlisten!(server, reference, Keyword.t()) :: :ok 185 | def unlisten!(pid, ref, opts \\ []) do 186 | case unlisten(pid, ref, opts) do 187 | :ok -> :ok 188 | :error -> raise ArgumentError, "unknown reference #{inspect(ref)}" 189 | end 190 | end 191 | 192 | ## CALLBACKS ## 193 | 194 | @impl true 195 | def init(args) do 196 | {:ok, struct!(__MODULE__, args)} 197 | end 198 | 199 | @impl true 200 | def notify(channel, payload, state) do 201 | for {ref, pid} <- Map.get(state.listener_channels, channel, []) do 202 | send(pid, {:notification, self(), ref, channel, payload}) 203 | end 204 | 205 | :ok 206 | end 207 | 208 | @impl true 209 | def handle_connect(state) do 210 | state = %{state | connected: true} 211 | 212 | if map_size(state.listener_channels) > 0 do 213 | listen_statements = 214 | state.listener_channels 215 | |> Map.keys() 216 | |> Enum.map_join("\n", &~s(LISTEN "#{&1}";)) 217 | 218 | query = "DO $$BEGIN #{listen_statements} END$$" 219 | 220 | {:query, query, state} 221 | else 222 | {:noreply, state} 223 | end 224 | end 225 | 226 | @impl true 227 | def handle_disconnect(state) do 228 | state = %{state | connected: false} 229 | 230 | if state.auto_reconnect && state.from && state.ref do 231 | SimpleConnection.reply(state.from, {:eventually, state.ref}) 232 | 233 | {:noreply, %{state | from: nil, ref: nil}} 234 | else 235 | {:noreply, state} 236 | end 237 | end 238 | 239 | @impl true 240 | def handle_call({:listen, channel}, {pid, _} = from, state) do 241 | ref = Process.monitor(pid) 242 | 243 | state = put_in(state.listeners[ref], {channel, pid}) 244 | state = update_in(state.listener_channels[channel], &Map.put(&1 || %{}, ref, pid)) 245 | 246 | cond do 247 | not state.connected -> 248 | SimpleConnection.reply(from, {:eventually, ref}) 249 | 250 | {:noreply, state} 251 | 252 | map_size(state.listener_channels[channel]) == 1 -> 253 | {:query, ~s(LISTEN "#{channel}"), %{state | from: from, ref: ref}} 254 | 255 | true -> 256 | SimpleConnection.reply(from, {:ok, ref}) 257 | 258 | {:noreply, state} 259 | end 260 | end 261 | 262 | def handle_call({:unlisten, ref}, from, state) do 263 | case state.listeners do 264 | %{^ref => {channel, _pid}} -> 265 | Process.demonitor(ref, [:flush]) 266 | 267 | {_, state} = pop_in(state.listeners[ref]) 268 | {_, state} = pop_in(state.listener_channels[channel][ref]) 269 | 270 | if map_size(state.listener_channels[channel]) == 0 do 271 | {_, state} = pop_in(state.listener_channels[channel]) 272 | 273 | {:query, ~s(UNLISTEN "#{channel}"), %{state | from: from}} 274 | else 275 | from && SimpleConnection.reply(from, :ok) 276 | 277 | {:noreply, state} 278 | end 279 | 280 | _ -> 281 | from && SimpleConnection.reply(from, :error) 282 | 283 | {:noreply, state} 284 | end 285 | end 286 | 287 | @impl true 288 | def handle_info({:DOWN, ref, :process, _, _}, state) do 289 | handle_call({:unlisten, ref}, nil, state) 290 | end 291 | 292 | def handle_info(msg, state) do 293 | Logger.info(fn -> 294 | context = " received unexpected message: " 295 | [inspect(__MODULE__), ?\s, inspect(self()), context | inspect(msg)] 296 | end) 297 | 298 | {:noreply, state} 299 | end 300 | 301 | @impl true 302 | def handle_result(_message, %{from: from, ref: ref} = state) do 303 | cond do 304 | from && ref -> 305 | SimpleConnection.reply(from, {:ok, ref}) 306 | 307 | {:noreply, %{state | from: nil, ref: nil}} 308 | 309 | from -> 310 | SimpleConnection.reply(from, :ok) 311 | 312 | {:noreply, %{state | from: nil}} 313 | 314 | true -> 315 | {:noreply, state} 316 | end 317 | end 318 | end 319 | -------------------------------------------------------------------------------- /lib/postgrex/parameters.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Parameters do 2 | @moduledoc false 3 | 4 | use GenServer 5 | 6 | defstruct [] 7 | @type t :: %__MODULE__{} 8 | 9 | def start_link(_) do 10 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 11 | end 12 | 13 | @spec insert(%{binary => binary}) :: reference 14 | def insert(parameters) do 15 | GenServer.call(__MODULE__, {:insert, parameters}) 16 | end 17 | 18 | @spec put(reference, binary, binary) :: boolean 19 | def put(ref, name, value) do 20 | try do 21 | :ets.lookup_element(__MODULE__, ref, 2) 22 | rescue 23 | ArgumentError -> 24 | false 25 | else 26 | parameters -> 27 | parameters = Map.put(parameters, name, value) 28 | :ets.update_element(__MODULE__, ref, {2, parameters}) 29 | end 30 | end 31 | 32 | @spec delete(reference) :: :ok 33 | def delete(ref) do 34 | GenServer.cast(__MODULE__, {:delete, ref}) 35 | end 36 | 37 | @spec fetch(reference) :: {:ok, %{binary => binary}} | :error 38 | def fetch(ref) do 39 | try do 40 | :ets.lookup_element(__MODULE__, ref, 2) 41 | rescue 42 | ArgumentError -> 43 | :error 44 | else 45 | parameters -> 46 | {:ok, parameters} 47 | end 48 | end 49 | 50 | def init(nil) do 51 | opts = [:public, :named_table, {:read_concurrency, true}, {:write_concurrency, true}] 52 | state = :ets.new(__MODULE__, opts) 53 | {:ok, state} 54 | end 55 | 56 | def handle_call({:insert, parameters}, {pid, _}, state) do 57 | ref = Process.monitor(pid) 58 | true = :ets.insert_new(state, {ref, parameters}) 59 | {:reply, ref, state} 60 | end 61 | 62 | def handle_cast({:delete, ref}, state) do 63 | :ets.delete(state, ref) 64 | {:noreply, state} 65 | end 66 | 67 | def handle_info({:DOWN, ref, :process, _, _}, state) do 68 | :ets.delete(state, ref) 69 | {:noreply, state} 70 | end 71 | end 72 | 73 | defimpl DBConnection.Query, for: Postgrex.Parameters do 74 | def parse(query, _), do: query 75 | def describe(query, _), do: query 76 | def encode(_, nil, _), do: nil 77 | def decode(_, parameters, _), do: parameters 78 | end 79 | -------------------------------------------------------------------------------- /lib/postgrex/query.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Query do 2 | @moduledoc """ 3 | Query struct returned from a successfully prepared query. 4 | 5 | Its public fields are: 6 | 7 | * `name` - The name of the prepared statement; 8 | * `statement` - The prepared statement; 9 | * `columns` - The column names; 10 | * `ref` - A reference used to identify prepared queries; 11 | 12 | ## Prepared queries 13 | 14 | Once a query is prepared with `Postgrex.prepare/4`, the 15 | returned query will have its `ref` field set to a reference. 16 | When `Postgrex.execute/4` is called with the prepared query, 17 | it always returns a query. If the `ref` field in the query 18 | given to `execute` and the one returned are the same, it 19 | means the cached prepared query was used. If the `ref` field 20 | is not the same, it means the query had to be re-prepared. 21 | """ 22 | 23 | @type t :: %__MODULE__{ 24 | cache: :reference | :statement, 25 | ref: reference | nil, 26 | name: iodata, 27 | statement: iodata, 28 | param_oids: [Postgrex.Types.oid()] | nil, 29 | param_formats: [:binary | :text] | nil, 30 | param_types: [Postgrex.Types.type()] | nil, 31 | columns: [String.t()] | nil, 32 | result_oids: [Postgrex.Types.oid()] | nil, 33 | result_formats: [:binary | :text] | nil, 34 | result_types: [Postgrex.Types.type()] | nil, 35 | types: Postgrex.Types.state() | nil 36 | } 37 | 38 | defstruct [ 39 | :ref, 40 | :name, 41 | :statement, 42 | :param_oids, 43 | :param_formats, 44 | :param_types, 45 | :columns, 46 | :result_oids, 47 | :result_formats, 48 | :result_types, 49 | :types, 50 | cache: :reference 51 | ] 52 | end 53 | 54 | defimpl DBConnection.Query, for: Postgrex.Query do 55 | require Postgrex.Messages 56 | 57 | def parse(%{types: nil, name: name} = query, _) do 58 | # for query table to match names must be equal 59 | %{query | name: IO.iodata_to_binary(name)} 60 | end 61 | 62 | def parse(query, _) do 63 | raise ArgumentError, "query #{inspect(query)} has already been prepared" 64 | end 65 | 66 | def describe(query, _), do: query 67 | 68 | def encode(%{types: nil} = query, _params, _) do 69 | raise ArgumentError, "query #{inspect(query)} has not been prepared" 70 | end 71 | 72 | def encode(query, params, _) do 73 | %{param_types: param_types, types: types} = query 74 | 75 | case Postgrex.Types.encode_params(params, param_types, types) do 76 | encoded when is_list(encoded) -> 77 | encoded 78 | 79 | :error -> 80 | raise ArgumentError, 81 | "parameters must be of length #{length(param_types)} for query #{inspect(query)}" 82 | end 83 | end 84 | 85 | def decode(_, %Postgrex.Result{rows: nil} = res, _opts) do 86 | res 87 | end 88 | 89 | def decode(_, %Postgrex.Result{rows: rows} = res, opts) do 90 | %{res | rows: decode_map(rows, opts)} 91 | end 92 | 93 | def decode(_, %Postgrex.Copy{} = copy, _opts) do 94 | copy 95 | end 96 | 97 | ## Helpers 98 | 99 | defp decode_map(data, opts) do 100 | case opts[:decode_mapper] do 101 | nil -> Enum.reverse(data) 102 | mapper -> decode_map(data, mapper, []) 103 | end 104 | end 105 | 106 | defp decode_map([row | data], mapper, decoded) do 107 | decode_map(data, mapper, [mapper.(row) | decoded]) 108 | end 109 | 110 | defp decode_map([], _, decoded) do 111 | decoded 112 | end 113 | end 114 | 115 | defimpl String.Chars, for: Postgrex.Query do 116 | def to_string(%Postgrex.Query{statement: statement}) do 117 | IO.iodata_to_binary(statement) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/postgrex/result.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Result do 2 | @moduledoc """ 3 | Result struct returned from any successful query. Its fields are: 4 | 5 | * `command` - An atom or a list of atoms of the query command, for example: 6 | `:select`, `:insert`, or `[:rollback, :release]`; 7 | * `columns` - The column names; 8 | * `rows` - The result set. A list of lists, each inner list corresponding to a 9 | row, each element in the inner list corresponds to a column; 10 | * `num_rows` - The number of fetched or affected rows; 11 | * `connection_id` - The OS pid of the PostgreSQL backend that executed the query; 12 | * `messages` - A list of maps of messages, such as hints and notices, sent by the 13 | driver during the execution of the query. 14 | """ 15 | 16 | @type t :: %__MODULE__{ 17 | command: atom | [atom], 18 | columns: [String.t()] | nil, 19 | rows: [[term] | binary] | nil, 20 | num_rows: integer, 21 | connection_id: pos_integer, 22 | messages: [map()] 23 | } 24 | 25 | defstruct [:command, :columns, :rows, :num_rows, :connection_id, messages: nil] 26 | end 27 | 28 | if Code.ensure_loaded?(Table.Reader) do 29 | defimpl Table.Reader, for: Postgrex.Result do 30 | def init(%{columns: columns}) when columns in [nil, []] do 31 | {:rows, %{columns: [], count: 0}, []} 32 | end 33 | 34 | def init(result) do 35 | {columns, _} = 36 | Enum.map_reduce(result.columns, %{}, fn column, counts -> 37 | counts = Map.update(counts, column, 1, &(&1 + 1)) 38 | 39 | case counts[column] do 40 | 1 -> {column, counts} 41 | n -> {"#{column}_#{n}", counts} 42 | end 43 | end) 44 | 45 | {:rows, %{columns: columns, count: result.num_rows}, result.rows} 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/postgrex/scram.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.SCRAM do 2 | @moduledoc false 3 | 4 | alias Postgrex.SCRAM 5 | 6 | @hash_length 32 7 | @nonce_length 24 8 | @nonce_rand_bytes div(@nonce_length * 6, 8) 9 | @nonce_prefix "n,,n=,r=" 10 | @nonce_encoded_size <> 11 | 12 | def client_first do 13 | nonce = @nonce_rand_bytes |> :crypto.strong_rand_bytes() |> Base.encode64() 14 | ["SCRAM-SHA-256", 0, @nonce_encoded_size, @nonce_prefix, nonce] 15 | end 16 | 17 | def client_final(data, opts) do 18 | # Extract data from server-first message 19 | server = parse_server_data(data) 20 | {:ok, server_s} = Base.decode64(server[?s]) 21 | server_i = String.to_integer(server[?i]) 22 | 23 | # Create and cache client and server keys if they don't already exist 24 | pass = Keyword.fetch!(opts, :password) 25 | cache_key = create_cache_key(pass, server_s, server_i) 26 | 27 | {client_key, _server_key} = 28 | SCRAM.LockedCache.run(cache_key, fn -> 29 | calculate_client_server_keys(pass, server_s, server_i) 30 | end) 31 | 32 | # Construct client signature and proof 33 | message_without_proof = ["c=biws,r=", server[?r]] 34 | client_nonce = binary_part(server[?r], 0, @nonce_length) 35 | message = ["n=,r=", client_nonce, ",r=", server[?r], ",s=", server[?s], ",i=", server[?i], ?,] 36 | auth_message = IO.iodata_to_binary([message | message_without_proof]) 37 | 38 | client_sig = hmac(:sha256, :crypto.hash(:sha256, client_key), auth_message) 39 | proof = Base.encode64(:crypto.exor(client_key, client_sig)) 40 | 41 | # Store data needed to verify the server signature 42 | scram_state = %{salt: server_s, iterations: server_i, auth_message: auth_message} 43 | 44 | {[message_without_proof, ",p=", proof], scram_state} 45 | end 46 | 47 | def verify_server(data, scram_state, opts) do 48 | data 49 | |> parse_server_data() 50 | |> do_verify_server(scram_state, opts) 51 | end 52 | 53 | defp do_verify_server(%{?e => server_e}, _scram_state, _opts) do 54 | msg = "error received in SCRAM server final message: #{inspect(server_e)}" 55 | {:error, %Postgrex.Error{message: msg}} 56 | end 57 | 58 | defp do_verify_server(%{?v => server_v}, scram_state, opts) do 59 | # Decode server signature from the server-final message 60 | {:ok, server_sig} = Base.decode64(server_v) 61 | 62 | # Construct expected server signature 63 | pass = Keyword.fetch!(opts, :password) 64 | cache_key = create_cache_key(pass, scram_state.salt, scram_state.iterations) 65 | {_client_key, server_key} = SCRAM.LockedCache.get(cache_key) 66 | expected_server_sig = hmac(:sha256, server_key, scram_state.auth_message) 67 | 68 | # Verify the server signature sent to us is correct 69 | if expected_server_sig == server_sig do 70 | :ok 71 | else 72 | msg = "cannot verify SCRAM server signature" 73 | {:error, %Postgrex.Error{message: msg}} 74 | end 75 | end 76 | 77 | defp do_verify_server(server, _scram_state, _opts) do 78 | msg = "unsupported SCRAM server final message: #{inspect(server)}" 79 | {:error, %Postgrex.Error{message: msg}} 80 | end 81 | 82 | defp parse_server_data(data) do 83 | for kv <- :binary.split(data, ",", [:global]), into: %{} do 84 | <= @hash_length do 107 | acc 108 | |> IO.iodata_to_binary() 109 | |> binary_part(0, @hash_length) 110 | end 111 | 112 | defp hash_password(secret, salt, iterations, block_index, acc, length) do 113 | initial = hmac(:sha256, secret, <>) 114 | block = iterate(secret, iterations - 1, initial, initial) 115 | length = byte_size(block) + length 116 | hash_password(secret, salt, iterations, block_index + 1, [acc | block], length) 117 | end 118 | 119 | defp iterate(_secret, 0, _prev, acc), do: acc 120 | 121 | defp iterate(secret, iteration, prev, acc) do 122 | next = hmac(:sha256, secret, prev) 123 | iterate(secret, iteration - 1, next, :crypto.exor(next, acc)) 124 | end 125 | 126 | # :crypto.mac/4 was added in OTP-22.1, and :crypto.hmac/3 removed in OTP-24. 127 | # Check which function to use at compile time to avoid doing a round-trip 128 | # to the code server on every call. The downside is this module won't work 129 | # if it's compiled on OTP-22.0 or older then executed on OTP-24 or newer. 130 | if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :mac, 4) do 131 | defp hmac(type, key, data), do: :crypto.mac(:hmac, type, key, data) 132 | else 133 | defp hmac(type, key, data), do: :crypto.hmac(type, key, data) 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/postgrex/scram/locked_cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.SCRAM.LockedCache do 2 | @moduledoc false 3 | 4 | # SCRAM authentication requires expensive calculations 5 | # that may be repeated across multiple connections. 6 | # This module provides a cache functionality so that 7 | # those are done only once, even if concurrently. 8 | # 9 | # Since those resources can be created dynamically, 10 | # multiple times, they are stored in ETS instead of 11 | # persistent term. 12 | use GenServer 13 | 14 | @name __MODULE__ 15 | @timeout :infinity 16 | 17 | @doc """ 18 | Reads the cache key. 19 | """ 20 | def get(key) do 21 | soft_read(key) 22 | end 23 | 24 | @doc """ 25 | Reads cache key or executes the given function if not 26 | cached yet. 27 | """ 28 | def run(key, fun) do 29 | try do 30 | hard_read(key) 31 | catch 32 | :error, :badarg -> 33 | case GenServer.call(@name, {:lock, key}, @timeout) do 34 | {:uncached, ref} -> 35 | try do 36 | fun.() 37 | catch 38 | kind, reason -> 39 | GenServer.cast(@name, {:uncached, ref}) 40 | :erlang.raise(kind, reason, __STACKTRACE__) 41 | else 42 | result -> 43 | write(key, result) 44 | GenServer.cast(@name, {:cached, ref}) 45 | result 46 | end 47 | 48 | :cached -> 49 | hard_read(key) 50 | end 51 | end 52 | end 53 | 54 | defp init(), do: :ets.new(@name, [:public, :set, :named_table, read_concurrency: true]) 55 | defp write(key, value), do: :ets.insert(@name, {key, value}) 56 | defp hard_read(key), do: :ets.lookup_element(@name, key, 2) 57 | 58 | defp soft_read(key) do 59 | try do 60 | :ets.lookup_element(@name, key, 2) 61 | catch 62 | :error, :badarg -> nil 63 | end 64 | end 65 | 66 | ## Callbacks 67 | 68 | @doc false 69 | def start_link(_opts) do 70 | GenServer.start_link(__MODULE__, :ok, name: @name) 71 | end 72 | 73 | @impl true 74 | def init(:ok) do 75 | init() 76 | {:ok, %{keys: %{}, ref_to_key: %{}}} 77 | end 78 | 79 | @impl true 80 | def handle_call({:lock, key}, from, state) do 81 | case state.keys do 82 | %{^key => {ref, waiting}} -> 83 | {:noreply, put_in(state.keys[key], {ref, [from | waiting]})} 84 | 85 | %{} -> 86 | {:noreply, lock(key, from, [], state)} 87 | end 88 | end 89 | 90 | @impl true 91 | def handle_cast({:cached, ref}, state) do 92 | Process.demonitor(ref, [:flush]) 93 | {key, state} = pop_in(state.ref_to_key[ref]) 94 | {{^ref, waiting}, state} = pop_in(state.keys[key]) 95 | for from <- waiting, do: GenServer.reply(from, :cached) 96 | {:noreply, state} 97 | end 98 | 99 | @impl true 100 | def handle_cast({:uncached, ref}, state) do 101 | Process.demonitor(ref, [:flush]) 102 | {:noreply, unlock(ref, state)} 103 | end 104 | 105 | @impl true 106 | def handle_info({:DOWN, ref, _, _, _}, state) do 107 | {:noreply, unlock(ref, state)} 108 | end 109 | 110 | defp lock(key, {pid, _} = from, waiting, state) do 111 | ref = Process.monitor(pid) 112 | state = put_in(state.keys[key], {ref, waiting}) 113 | state = put_in(state.ref_to_key[ref], key) 114 | GenServer.reply(from, {:uncached, ref}) 115 | state 116 | end 117 | 118 | defp unlock(ref, state) do 119 | {key, state} = pop_in(state.ref_to_key[ref]) 120 | {{^ref, waiting}, state} = pop_in(state.keys[key]) 121 | 122 | case waiting do 123 | [] -> state 124 | [from | waiting] -> lock(key, from, waiting, state) 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/postgrex/stream.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Stream do 2 | @moduledoc """ 3 | Stream struct returned from stream commands. 4 | 5 | All of its fields are private. 6 | """ 7 | @derive {Inspect, only: []} 8 | defstruct [:conn, :query, :params, :options] 9 | @type t :: %Postgrex.Stream{} 10 | end 11 | 12 | defmodule Postgrex.Cursor do 13 | @moduledoc false 14 | defstruct [:portal, :ref, :connection_id, :mode] 15 | @type t :: %Postgrex.Cursor{} 16 | end 17 | 18 | defmodule Postgrex.Copy do 19 | @moduledoc false 20 | defstruct [:portal, :ref, :connection_id, :query] 21 | @type t :: %Postgrex.Copy{} 22 | end 23 | 24 | defimpl Enumerable, for: Postgrex.Stream do 25 | alias Postgrex.Query 26 | 27 | def reduce(%Postgrex.Stream{query: %Query{} = query} = stream, acc, fun) do 28 | %Postgrex.Stream{conn: conn, params: params, options: opts} = stream 29 | stream = %DBConnection.Stream{conn: conn, query: query, params: params, opts: opts} 30 | DBConnection.reduce(stream, acc, fun) 31 | end 32 | 33 | def reduce(%Postgrex.Stream{query: statement} = stream, acc, fun) do 34 | %Postgrex.Stream{conn: conn, params: params, options: opts} = stream 35 | query = %Query{name: "", statement: statement} 36 | opts = Keyword.put(opts, :function, :prepare_open) 37 | stream = %DBConnection.PrepareStream{conn: conn, query: query, params: params, opts: opts} 38 | DBConnection.reduce(stream, acc, fun) 39 | end 40 | 41 | def member?(_, _) do 42 | {:error, __MODULE__} 43 | end 44 | 45 | def count(_) do 46 | {:error, __MODULE__} 47 | end 48 | 49 | def slice(_) do 50 | {:error, __MODULE__} 51 | end 52 | end 53 | 54 | defimpl Collectable, for: Postgrex.Stream do 55 | alias Postgrex.Stream 56 | alias Postgrex.Query 57 | 58 | def into(%Stream{conn: %DBConnection{}} = stream) do 59 | %Stream{conn: conn, query: query, params: params, options: opts} = stream 60 | opts = Keyword.put(opts, :postgrex_copy, true) 61 | 62 | case query do 63 | %Query{} -> 64 | copy = DBConnection.execute!(conn, query, params, opts) 65 | {:ok, make_into(conn, stream, copy, opts)} 66 | 67 | query -> 68 | query = %Query{name: "", statement: query} 69 | {_, copy} = DBConnection.prepare_execute!(conn, query, params, opts) 70 | {:ok, make_into(conn, stream, copy, opts)} 71 | end 72 | end 73 | 74 | def into(_) do 75 | raise ArgumentError, "data can only be copied to database inside a transaction" 76 | end 77 | 78 | defp make_into(conn, stream, %Postgrex.Copy{ref: ref} = copy, opts) do 79 | fn 80 | :ok, {:cont, data} -> 81 | _ = DBConnection.execute!(conn, copy, {:copy_data, ref, data}, opts) 82 | :ok 83 | 84 | :ok, close when close in [:done, :halt] -> 85 | _ = DBConnection.execute!(conn, copy, {:copy_done, ref}, opts) 86 | stream 87 | end 88 | end 89 | end 90 | 91 | defimpl DBConnection.Query, for: Postgrex.Copy do 92 | alias Postgrex.Copy 93 | import Postgrex.Messages 94 | 95 | def parse(copy, _) do 96 | raise "can not prepare #{inspect(copy)}" 97 | end 98 | 99 | def describe(copy, _) do 100 | raise "can not describe #{inspect(copy)}" 101 | end 102 | 103 | def encode(%Copy{ref: ref}, {:copy_data, ref, data}, _) do 104 | try do 105 | encode_msg(msg_copy_data(data: data)) 106 | rescue 107 | ArgumentError -> 108 | reraise ArgumentError, 109 | [message: "expected iodata to copy to database, got: " <> inspect(data)], 110 | __STACKTRACE__ 111 | else 112 | iodata -> 113 | {:copy_data, iodata} 114 | end 115 | end 116 | 117 | def encode(%Copy{ref: ref}, {:copy_done, ref}, _) do 118 | :copy_done 119 | end 120 | 121 | def decode(copy, _result, _opts) do 122 | raise "can not describe #{inspect(copy)}" 123 | end 124 | end 125 | 126 | defimpl String.Chars, for: Postgrex.Copy do 127 | def to_string(%Postgrex.Copy{query: query}) do 128 | String.Chars.to_string(query) 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/postgrex/super_extension.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.SuperExtension do 2 | @moduledoc false 3 | 4 | @type state :: term 5 | 6 | @callback init(Keyword.t()) :: state 7 | 8 | @callback matching(state) :: [ 9 | type: String.t(), 10 | send: String.t(), 11 | receive: String.t(), 12 | input: String.t(), 13 | output: String.t() 14 | ] 15 | 16 | @callback format(state) :: :super_binary 17 | 18 | @callback oids(Postgrex.TypeInfo.t(), state) :: nil | [Postgrex.Types.oid()] 19 | 20 | @callback encode(state) :: Macro.t() 21 | 22 | @callback decode(state) :: Macro.t() 23 | end 24 | -------------------------------------------------------------------------------- /lib/postgrex/type_info.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.TypeInfo do 2 | @moduledoc """ 3 | The information about a type that is provided to the custom encoder/decoder 4 | functions. See http://www.postgresql.org/docs/9.4/static/catalog-pg-type.html 5 | for clarifications of the fields. 6 | 7 | * `oid` - The type's id; 8 | * `type` - The type name; 9 | * `send` - The name of the "send" function (the function postgres uses 10 | to convert the type to its binary format); 11 | * `receive` - The name of the "receive" function (the function postgres uses 12 | to convert the type from its binary format); 13 | * `output` - The name of the "output" function (the function postgres uses 14 | to convert the type to its text format); 15 | * `input` - The name of the "input" function (the function postgres uses 16 | to convert the type from its text format); 17 | * `array_elem` - If this is an array, the array elements' oid; 18 | * `base_type` - If this is a range type, the base type's oid; 19 | * `comp_elems` - If this is a composite type (record), the tuple 20 | elements' oid; 21 | """ 22 | 23 | alias Postgrex.Types 24 | 25 | @type t :: %__MODULE__{ 26 | oid: Types.oid(), 27 | type: String.t(), 28 | send: String.t(), 29 | receive: String.t(), 30 | output: String.t(), 31 | input: String.t(), 32 | array_elem: Types.oid(), 33 | base_type: Types.oid(), 34 | comp_elems: [Types.oid()] 35 | } 36 | 37 | defstruct [:oid, :type, :send, :receive, :output, :input, :array_elem, :base_type, :comp_elems] 38 | end 39 | -------------------------------------------------------------------------------- /lib/postgrex/type_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.TypeServer do 2 | @moduledoc false 3 | 4 | use GenServer, restart: :temporary 5 | 6 | defstruct [:types, :connections, :lock, :waiting] 7 | 8 | @timeout 60_000 9 | 10 | @doc """ 11 | Starts a type server. 12 | """ 13 | @spec start_link({module, pid, keyword}) :: GenServer.on_start() 14 | def start_link({module, starter, opts}) do 15 | GenServer.start_link(__MODULE__, {module, starter}, opts) 16 | end 17 | 18 | @doc """ 19 | Fetches a lock for the given type server. 20 | 21 | We attempt to achieve a lock on the type server for updating the entries. 22 | If another process got the lock we wait for it to finish. 23 | """ 24 | @spec fetch(pid) :: 25 | {:lock, reference, Postgrex.Types.state()} | :noproc | :error 26 | def fetch(server) do 27 | try do 28 | GenServer.call(server, :fetch, @timeout) 29 | catch 30 | # module timed out, pretend it did not exist. 31 | :exit, {:normal, _} -> :noproc 32 | :exit, {:noproc, _} -> :noproc 33 | end 34 | end 35 | 36 | @doc """ 37 | Update the type server using the given reference and configuration. 38 | """ 39 | @spec update(pid, reference, [Postgrex.TypeInfo.t()]) :: :ok 40 | def update(server, ref, [_ | _] = type_infos) do 41 | GenServer.call(server, {:update, ref, type_infos}, @timeout) 42 | end 43 | 44 | def update(server, ref, []) do 45 | done(server, ref) 46 | end 47 | 48 | @doc """ 49 | Unlocks the given reference for a given module if no update. 50 | """ 51 | @spec done(pid, reference) :: :ok 52 | def done(server, ref) do 53 | GenServer.cast(server, {:done, ref}) 54 | end 55 | 56 | ## Callbacks 57 | 58 | def init({module, starter}) do 59 | _ = Process.flag(:trap_exit, true) 60 | Process.link(starter) 61 | 62 | state = %__MODULE__{ 63 | types: Postgrex.Types.new(module), 64 | connections: MapSet.new([starter]), 65 | waiting: :queue.new() 66 | } 67 | 68 | {:ok, state} 69 | end 70 | 71 | def handle_call(:fetch, from, %{lock: nil} = state) do 72 | lock(state, from) 73 | end 74 | 75 | def handle_call(:fetch, from, %{lock: ref} = state) when is_reference(ref) do 76 | wait(state, from) 77 | end 78 | 79 | def handle_call({:update, ref, type_infos}, from, %{lock: ref} = state) 80 | when is_reference(ref) do 81 | associate(state, type_infos, from) 82 | end 83 | 84 | def handle_cast({:done, ref}, %{lock: ref} = state) when is_reference(ref) do 85 | Process.demonitor(ref, [:flush]) 86 | next(state) 87 | end 88 | 89 | def handle_info({:DOWN, ref, _, _, _}, %{lock: ref} = state) 90 | when is_reference(ref) do 91 | next(state) 92 | end 93 | 94 | def handle_info({:DOWN, ref, _, _, _}, state) do 95 | down(state, ref) 96 | end 97 | 98 | def handle_info({:EXIT, pid, _}, state) do 99 | exit(state, pid) 100 | end 101 | 102 | def handle_info(:timeout, state) do 103 | {:stop, :normal, state} 104 | end 105 | 106 | ## Helpers 107 | 108 | defp lock(%{connections: connections, types: types} = state, {pid, _}) do 109 | Process.link(pid) 110 | mref = Process.monitor(pid) 111 | state = %{state | lock: mref, connections: MapSet.put(connections, pid)} 112 | {:reply, {:lock, mref, types}, state} 113 | end 114 | 115 | defp wait(state, {pid, _} = from) do 116 | %{connections: connections, waiting: waiting} = state 117 | Process.link(pid) 118 | mref = Process.monitor(pid) 119 | 120 | state = %{ 121 | state 122 | | connections: MapSet.put(connections, pid), 123 | waiting: :queue.in({mref, from}, waiting) 124 | } 125 | 126 | {:noreply, state} 127 | end 128 | 129 | defp associate(%{types: types, lock: ref} = state, type_infos, from) do 130 | Postgrex.Types.associate_type_infos(type_infos, types) 131 | Process.demonitor(ref, [:flush]) 132 | GenServer.reply(from, :go) 133 | next(state) 134 | end 135 | 136 | defp next(%{types: types, waiting: waiting} = state) do 137 | case :queue.out(waiting) do 138 | {{:value, {mref, from}}, waiting} -> 139 | GenServer.reply(from, {:lock, mref, types}) 140 | {:noreply, %{state | lock: mref, waiting: waiting}} 141 | 142 | {:empty, waiting} -> 143 | check_processes(%{state | lock: nil, waiting: waiting}) 144 | end 145 | end 146 | 147 | defp down(%{waiting: waiting} = state, ref) do 148 | check_processes(%{state | waiting: :queue.filter(fn {mref, _} -> mref != ref end, waiting)}) 149 | end 150 | 151 | defp exit(%{connections: connections} = state, pid) do 152 | check_processes(%{state | connections: MapSet.delete(connections, pid)}) 153 | end 154 | 155 | defp check_processes(%{lock: ref} = state) when is_reference(ref) do 156 | {:noreply, state} 157 | end 158 | 159 | defp check_processes(%{connections: connections} = state) do 160 | case MapSet.size(connections) do 161 | 0 -> 162 | timeout = Application.fetch_env!(:postgrex, :type_server_reap_after) 163 | {:noreply, state, timeout} 164 | 165 | _ -> 166 | {:noreply, state} 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/postgrex/type_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.TypeSupervisor do 2 | @moduledoc false 3 | 4 | use Supervisor 5 | 6 | @manager Postgrex.TypeManager 7 | @supervisor Postgrex.TypeSupervisor 8 | 9 | @doc """ 10 | Starts a type supervisor with a manager and a simple 11 | one for one supervisor for each server. 12 | """ 13 | def start_link(_) do 14 | Supervisor.start_link(__MODULE__, :ok) 15 | end 16 | 17 | @doc """ 18 | Locates a type server for the given module-key pair. 19 | """ 20 | def locate(module, key) do 21 | pair = {module, key} 22 | 23 | case Registry.lookup(@manager, pair) do 24 | [{pid, _}] -> if Process.alive?(pid), do: pid, else: start_server(module, pair) 25 | [] -> start_server(module, pair) 26 | end 27 | end 28 | 29 | defp start_server(module, pair) do 30 | opts = [name: {:via, Registry, {Postgrex.TypeManager, pair}}] 31 | 32 | case DynamicSupervisor.start_child(@supervisor, {Postgrex.TypeServer, {module, self(), opts}}) do 33 | {:ok, pid} -> pid 34 | {:error, {:already_started, pid}} -> pid 35 | end 36 | end 37 | 38 | # Callbacks 39 | 40 | def init(:ok) do 41 | manager = {Registry, keys: :unique, name: @manager} 42 | server_sup = {DynamicSupervisor, strategy: :one_for_one, name: @supervisor} 43 | Supervisor.init([manager, server_sup], strategy: :rest_for_one) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/postgrex/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Utils do 2 | @moduledoc false 3 | 4 | @extensions [ 5 | Postgrex.Extensions.Array, 6 | Postgrex.Extensions.BitString, 7 | Postgrex.Extensions.Bool, 8 | Postgrex.Extensions.Box, 9 | Postgrex.Extensions.Circle, 10 | Postgrex.Extensions.Date, 11 | Postgrex.Extensions.Float4, 12 | Postgrex.Extensions.Float8, 13 | Postgrex.Extensions.HStore, 14 | Postgrex.Extensions.INET, 15 | Postgrex.Extensions.Int2, 16 | Postgrex.Extensions.Int4, 17 | Postgrex.Extensions.Int8, 18 | Postgrex.Extensions.Interval, 19 | Postgrex.Extensions.JSON, 20 | Postgrex.Extensions.JSONB, 21 | Postgrex.Extensions.Line, 22 | Postgrex.Extensions.LineSegment, 23 | Postgrex.Extensions.Lquery, 24 | Postgrex.Extensions.Ltree, 25 | Postgrex.Extensions.Ltxtquery, 26 | Postgrex.Extensions.MACADDR, 27 | Postgrex.Extensions.Multirange, 28 | Postgrex.Extensions.Name, 29 | Postgrex.Extensions.Numeric, 30 | Postgrex.Extensions.OID, 31 | Postgrex.Extensions.Path, 32 | Postgrex.Extensions.Point, 33 | Postgrex.Extensions.Polygon, 34 | Postgrex.Extensions.Range, 35 | Postgrex.Extensions.Raw, 36 | Postgrex.Extensions.Record, 37 | Postgrex.Extensions.TID, 38 | Postgrex.Extensions.Time, 39 | Postgrex.Extensions.Timestamp, 40 | Postgrex.Extensions.TimestampTZ, 41 | Postgrex.Extensions.TimeTZ, 42 | Postgrex.Extensions.TSVector, 43 | Postgrex.Extensions.UUID, 44 | Postgrex.Extensions.VoidBinary, 45 | Postgrex.Extensions.VoidText, 46 | Postgrex.Extensions.Xid8 47 | ] 48 | 49 | @doc """ 50 | Checks if a given extension is a default extension. 51 | """ 52 | for ext <- @extensions do 53 | def default_extension?(unquote(ext)), do: true 54 | end 55 | 56 | def default_extension?(_), do: false 57 | 58 | @doc """ 59 | List all default extensions. 60 | """ 61 | @spec default_extensions(Keyword.t()) :: [{module(), Keyword.t()}] 62 | def default_extensions(opts \\ []) do 63 | Enum.map(@extensions, &{&1, opts}) 64 | end 65 | 66 | @doc """ 67 | Converts pg major.minor.patch (http://www.postgresql.org/support/versioning) version to an integer 68 | """ 69 | def parse_version(version) do 70 | segments = 71 | version 72 | |> String.split(" ", parts: 2) 73 | |> hd() 74 | |> String.split(".", parts: 4) 75 | |> Enum.map(&parse_version_bit/1) 76 | 77 | case segments do 78 | [major, minor, patch, _] -> {major, minor, patch} 79 | [major, minor, patch] -> {major, minor, patch} 80 | [major, minor] -> {major, minor, 0} 81 | [major] -> {major, 0, 0} 82 | end 83 | end 84 | 85 | @doc """ 86 | Fills in the given `opts` with default options. 87 | Only adds keys extracted via PGHOST if no endpoint-related keys are explicitly provided. 88 | """ 89 | @spec default_opts(Keyword.t()) :: Keyword.t() 90 | def default_opts(opts) do 91 | {field, value} = extract_host(System.get_env("PGHOST")) 92 | 93 | endpoint_keys = [:socket, :socket_dir, :hostname, :endpoints] 94 | has_endpoint? = Enum.any?(endpoint_keys, &Keyword.has_key?(opts, &1)) 95 | 96 | opts 97 | |> Keyword.put_new(:username, System.get_env("PGUSER") || System.get_env("USER")) 98 | |> Keyword.put_new(:password, System.get_env("PGPASSWORD")) 99 | |> Keyword.put_new(:database, System.get_env("PGDATABASE")) 100 | |> then(fn opts -> 101 | if has_endpoint?, do: opts, else: Keyword.put(opts, field, value) 102 | end) 103 | |> Keyword.put_new(:port, System.get_env("PGPORT")) 104 | |> Keyword.update!(:port, &normalize_port/1) 105 | |> Keyword.put_new(:types, Postgrex.DefaultTypes) 106 | |> Enum.reject(fn {_k, v} -> is_nil(v) end) 107 | end 108 | 109 | defp extract_host("/" <> _ = dir), do: {:socket_dir, dir} 110 | defp extract_host(<> <> _ = dir) when d in ?a..?z or d in ?A..?Z, do: {:socket_dir, dir} 111 | defp extract_host("@" <> abstract_socket), do: {:socket, <<0>> <> abstract_socket} 112 | defp extract_host(host), do: {:hostname, host || "localhost"} 113 | 114 | defp normalize_port(port) when is_binary(port), do: String.to_integer(port) 115 | defp normalize_port(port), do: port 116 | 117 | @doc """ 118 | Return encode error message. 119 | """ 120 | def encode_msg(%Postgrex.TypeInfo{type: type}, observed, expected) do 121 | "Postgrex expected #{to_desc(expected)} that can be encoded/cast to " <> 122 | "type #{inspect(type)}, got #{inspect(observed)}. Please make sure the " <> 123 | "value you are passing matches the definition in your table or in your " <> 124 | "query or convert the value accordingly." 125 | end 126 | 127 | @doc """ 128 | Return encode error message. 129 | """ 130 | def encode_msg(%Date{calendar: calendar} = observed, _expected) when calendar != Calendar.ISO do 131 | "Postgrex expected a %Date{} in the `Calendar.ISO` calendar, got #{inspect(observed)}. " <> 132 | "Postgrex (and PostgreSQL) support dates in the `Calendar.ISO` calendar only." 133 | end 134 | 135 | def encode_msg(%NaiveDateTime{calendar: calendar} = observed, _expected) 136 | when calendar != Calendar.ISO do 137 | "Postgrex expected a %NaiveDateTime{} in the `Calendar.ISO` calendar, got #{inspect(observed)}. " <> 138 | "Postgrex (and PostgreSQL) support naive datetimes in the `Calendar.ISO` calendar only." 139 | end 140 | 141 | def encode_msg(%DateTime{calendar: calendar} = observed, _expected) 142 | when calendar != Calendar.ISO do 143 | "Postgrex expected a %DateTime{} in the `Calendar.ISO` calendar, got #{inspect(observed)}. " <> 144 | "Postgrex (and PostgreSQL) support datetimes in the `Calendar.ISO` calendar only." 145 | end 146 | 147 | def encode_msg(%Time{calendar: calendar} = observed, _expected) when calendar != Calendar.ISO do 148 | "Postgrex expected a %Time{} in the `Calendar.ISO` calendar, got #{inspect(observed)}. " <> 149 | "Postgrex (and PostgreSQL) support times in the `Calendar.ISO` calendar only." 150 | end 151 | 152 | def encode_msg(observed, expected) do 153 | "Postgrex expected #{to_desc(expected)}, got #{inspect(observed)}. " <> 154 | "Please make sure the value you are passing matches the definition in " <> 155 | "your table or in your query or convert the value accordingly." 156 | end 157 | 158 | @doc """ 159 | Return type error message. 160 | """ 161 | def type_msg(%Postgrex.TypeInfo{type: json}, module) 162 | when json in ["json", "jsonb"] do 163 | "type `#{json}` can not be handled by the types module #{inspect(module)}, " <> 164 | "it must define a `:json` library in its options to support JSON types" 165 | end 166 | 167 | def type_msg(%Postgrex.TypeInfo{type: type}, module) do 168 | "type `#{type}` can not be handled by the types module #{inspect(module)}" 169 | end 170 | 171 | ## Helpers 172 | 173 | defp parse_version_bit(bit) do 174 | {int, _} = Integer.parse(bit) 175 | int 176 | end 177 | 178 | defp to_desc(struct) when is_atom(struct), do: "%#{inspect(struct)}{}" 179 | defp to_desc(%Range{} = range), do: "an integer in #{inspect(range)}" 180 | defp to_desc({a, b}), do: to_desc(a) <> " or " <> to_desc(b) 181 | defp to_desc(desc) when is_binary(desc), do: desc 182 | end 183 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Postgrex.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/elixir-ecto/postgrex" 5 | @version "0.21.0-dev" 6 | 7 | def project do 8 | [ 9 | app: :postgrex, 10 | version: @version, 11 | elixir: "~> 1.13", 12 | deps: deps(), 13 | name: "Postgrex", 14 | description: "PostgreSQL driver for Elixir", 15 | docs: docs(), 16 | package: package(), 17 | xref: [exclude: [Jason]] 18 | ] 19 | end 20 | 21 | # Configuration for the OTP application 22 | def application do 23 | [ 24 | extra_applications: [:logger, :crypto, :ssl], 25 | mod: {Postgrex.App, []}, 26 | env: [type_server_reap_after: 3 * 60_000, json_library: Jason] 27 | ] 28 | end 29 | 30 | defp deps do 31 | [ 32 | {:ex_doc, ">= 0.0.0", only: :docs}, 33 | {:jason, "~> 1.0", optional: true}, 34 | {:table, "~> 0.1.0", optional: true}, 35 | {:decimal, "~> 1.5 or ~> 2.0"}, 36 | {:db_connection, "~> 2.1"} 37 | ] 38 | end 39 | 40 | defp docs do 41 | [ 42 | source_url: @source_url, 43 | source_ref: "v#{@version}", 44 | main: "readme", 45 | extras: ["README.md", "CHANGELOG.md"], 46 | skip_undefined_reference_warnings_on: ["CHANGELOG.md"], 47 | groups_for_modules: [ 48 | # Postgrex 49 | # Postgrex.Notifications 50 | # Postgrex.Query 51 | # Postgrex.ReplicationConnection 52 | # Postgrex.SimpleConnection 53 | # Postgrex.Stream 54 | # Postgrex.Result 55 | "Data Types": [ 56 | Postgrex.Box, 57 | Postgrex.Circle, 58 | Postgrex.INET, 59 | Postgrex.Interval, 60 | Postgrex.Lexeme, 61 | Postgrex.Line, 62 | Postgrex.LineSegment, 63 | Postgrex.MACADDR, 64 | Postgrex.Path, 65 | Postgrex.Point, 66 | Postgrex.Polygon, 67 | Postgrex.Range 68 | ], 69 | "Custom types and Extensions": [ 70 | Postgrex.DefaultTypes, 71 | Postgrex.Extension, 72 | Postgrex.TypeInfo, 73 | Postgrex.Types 74 | ] 75 | ] 76 | ] 77 | end 78 | 79 | defp package do 80 | [ 81 | maintainers: ["Eric Meadows-Jönsson", "James Fish"], 82 | licenses: ["Apache-2.0"], 83 | links: %{"GitHub" => @source_url} 84 | ] 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, 3 | "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 5 | "ex_doc": {:hex, :ex_doc, "0.37.0", "970f92b39e62c460aa8a367508e938f5e4da6e2ff3eaed3f8530b25870f45471", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "b0ee7f17373948e0cf471e59c3a0ee42f3bd1171c67d91eb3626456ef9c6202c"}, 6 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 7 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 8 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 9 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 11 | "table": {:hex, :table, "0.1.0", "f16104d717f960a623afb134a91339d40d8e11e0c96cfce54fee086b333e43f0", [:mix], [], "hexpm", "bf533d3606823ad8a7ee16f41941e5e6e0e42a20c4504cdf4cfabaaed1c8acb9"}, 12 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, 13 | } 14 | -------------------------------------------------------------------------------- /test/builtins_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BuiltinsTest do 2 | use ExUnit.Case, async: true 3 | 4 | describe "Postgrex.Interval" do 5 | alias Postgrex.Interval 6 | 7 | test "compare" do 8 | assert Interval.compare(%Interval{}, %Interval{}) == :eq 9 | 10 | assert Interval.compare( 11 | %Interval{microsecs: 1}, 12 | %Interval{microsecs: 2} 13 | ) == :lt 14 | 15 | assert Interval.compare( 16 | %Interval{microsecs: 2}, 17 | %Interval{microsecs: 1} 18 | ) == :gt 19 | 20 | assert Interval.compare( 21 | %Interval{secs: 1, microsecs: 9}, 22 | %Interval{secs: 2, microsecs: 9} 23 | ) == :lt 24 | 25 | assert Interval.compare( 26 | %Interval{secs: 2, microsecs: 9}, 27 | %Interval{secs: 1, microsecs: 9} 28 | ) == :gt 29 | 30 | assert Interval.compare( 31 | %Interval{days: 1, secs: 8, microsecs: 9}, 32 | %Interval{days: 2, secs: 8, microsecs: 9} 33 | ) == :lt 34 | 35 | assert Interval.compare( 36 | %Interval{days: 2, secs: 8, microsecs: 9}, 37 | %Interval{days: 1, secs: 8, microsecs: 9} 38 | ) == :gt 39 | 40 | assert Interval.compare( 41 | %Interval{months: 1, days: 7, secs: 8, microsecs: 9}, 42 | %Interval{months: 2, days: 7, secs: 8, microsecs: 9} 43 | ) == :lt 44 | 45 | assert Interval.compare( 46 | %Interval{months: 2, days: 7, secs: 8, microsecs: 9}, 47 | %Interval{months: 1, days: 7, secs: 8, microsecs: 9} 48 | ) == :gt 49 | end 50 | 51 | test "to_string" do 52 | assert Interval.to_string(%Interval{secs: 0}) == 53 | "0 seconds" 54 | 55 | assert Interval.to_string(%Interval{microsecs: 123}) == 56 | "0.000123 seconds" 57 | 58 | assert Interval.to_string(%Interval{secs: 123}) == 59 | "123 seconds" 60 | 61 | assert Interval.to_string(%Interval{secs: 1, microsecs: 123}) == 62 | "1.000123 seconds" 63 | 64 | assert Interval.to_string(%Interval{secs: 1, microsecs: 654_321}) == 65 | "1.654321 seconds" 66 | 67 | assert Interval.to_string(%Interval{days: 1, secs: 1, microsecs: 654_321}) == 68 | "1 day, 1.654321 seconds" 69 | 70 | assert Interval.to_string(%Interval{days: 2, secs: 1, microsecs: 654_321}) == 71 | "2 days, 1.654321 seconds" 72 | 73 | assert Interval.to_string(%Interval{months: 1, days: 1, secs: 1, microsecs: 654_321}) == 74 | "1 month, 1 day, 1.654321 seconds" 75 | 76 | assert Interval.to_string(%Interval{months: 2, days: 2, secs: 1, microsecs: 654_321}) == 77 | "2 months, 2 days, 1.654321 seconds" 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ClientTest do 2 | use ExUnit.Case 3 | import Postgrex.TestHelper 4 | import ExUnit.CaptureLog 5 | 6 | setup do 7 | opts = [database: "postgrex_test", backoff_type: :stop, max_restarts: 0] 8 | {:ok, pid} = Postgrex.start_link(opts) 9 | {:ok, [pid: pid, options: opts]} 10 | end 11 | 12 | test "active client timeout", context do 13 | conn = context[:pid] 14 | %Postgrex.Result{connection_id: connection_id} = Postgrex.query!(conn, "SELECT 42", []) 15 | 16 | Process.flag(:trap_exit, true) 17 | 18 | assert capture_log(fn -> 19 | assert [[_]] = query("SELECT pg_stat_get_activity($1)", [connection_id]) 20 | 21 | case query("SELECT pg_sleep(10)", [], timeout: 50) do 22 | %Postgrex.Error{postgres: %{message: "canceling statement due to user request"}} -> 23 | :ok 24 | 25 | %DBConnection.ConnectionError{message: "tcp recv: closed" <> _} -> 26 | :ok 27 | 28 | other -> 29 | flunk("unexpected result: #{inspect(other)}") 30 | end 31 | 32 | assert_receive {:EXIT, ^conn, :killed} 33 | end) =~ "disconnected: ** (DBConnection.ConnectionError)" 34 | 35 | :timer.sleep(500) 36 | {:ok, pid} = Postgrex.start_link(context[:options]) 37 | 38 | assert %Postgrex.Result{rows: []} = 39 | Postgrex.query!(pid, "SELECT pg_stat_get_activity($1)", [connection_id]) 40 | end 41 | 42 | test "active client DOWN", context do 43 | self_pid = self() 44 | conn = context[:pid] 45 | 46 | pid = 47 | spawn(fn -> 48 | send(self_pid, query("SELECT pg_sleep(0.2)", [])) 49 | end) 50 | 51 | :timer.sleep(100) 52 | Process.flag(:trap_exit, true) 53 | 54 | capture_log(fn -> 55 | Process.exit(pid, :shutdown) 56 | assert_receive {:EXIT, ^conn, :killed} 57 | end) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/custom_extensions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CustomExtensionsTest do 2 | use ExUnit.Case, async: true 3 | import Postgrex.TestHelper 4 | import ExUnit.CaptureLog 5 | alias Postgrex, as: P 6 | alias Postgrex.Result 7 | 8 | @types CustomExtensionsTypes 9 | 10 | defmodule BinaryExtension do 11 | @behaviour Postgrex.Extension 12 | 13 | def init([]), 14 | do: [] 15 | 16 | def matching([]), 17 | do: [send: "int4send"] 18 | 19 | def format([]), 20 | do: :binary 21 | 22 | def encode([]) do 23 | quote do 24 | int -> 25 | <<4::int32(), int + 1::int32()>> 26 | end 27 | end 28 | 29 | def decode([]) do 30 | quote do 31 | <<4::int32(), int::int32()>> -> int + 1 32 | end 33 | end 34 | end 35 | 36 | defmodule TextExtension do 37 | @behaviour Postgrex.Extension 38 | 39 | def init([]), 40 | do: {} 41 | 42 | def matching({}), 43 | do: [send: "float8send", send: "oidsend"] 44 | 45 | def format({}), 46 | do: :text 47 | 48 | def encode({}) do 49 | quote do 50 | value -> 51 | [<> | value] 52 | end 53 | end 54 | 55 | def decode({}) do 56 | quote do 57 | <> -> 58 | binary 59 | end 60 | end 61 | end 62 | 63 | defmodule BadExtension do 64 | @behaviour Postgrex.Extension 65 | 66 | def init([]), 67 | do: [] 68 | 69 | def matching([]), 70 | do: [send: "boolsend"] 71 | 72 | def format([]), 73 | do: :binary 74 | 75 | def prelude([]) do 76 | quote do 77 | @encode_msg "encode" 78 | @decode_msg "decode" 79 | end 80 | end 81 | 82 | def encode([]) do 83 | quote do 84 | _ -> 85 | raise @encode_msg 86 | end 87 | end 88 | 89 | def decode([]) do 90 | quote do 91 | <<1::int32(), _>> -> 92 | raise @decode_msg 93 | end 94 | end 95 | end 96 | 97 | setup_all do 98 | on_exit(fn -> 99 | :code.delete(@types) 100 | :code.purge(@types) 101 | end) 102 | 103 | extensions = [BinaryExtension, TextExtension, BadExtension] 104 | opts = [decode_binary: :reference, null: :custom] 105 | Postgrex.TypeModule.define(@types, extensions, opts) 106 | :ok 107 | end 108 | 109 | setup do 110 | opts = [database: "postgrex_test", backoff_type: :stop, max_restarts: 0, types: @types] 111 | {:ok, pid} = P.start_link(opts) 112 | {:ok, [pid: pid, options: opts]} 113 | end 114 | 115 | test "encode and decode", context do 116 | assert [[44]] = query("SELECT $1::int4", [42]) 117 | assert [[[44]]] = query("SELECT $1::int4[]", [[42]]) 118 | end 119 | 120 | test "encode and decode unknown type", context do 121 | assert [["23"]] = query("SELECT $1::oid", ["23"]) 122 | end 123 | 124 | test "encode and decode pushes error to client", context do 125 | Process.flag(:trap_exit, true) 126 | 127 | assert_raise RuntimeError, "encode", fn -> 128 | query("SELECT $1::boolean", [true]) 129 | end 130 | 131 | assert capture_log(fn -> 132 | assert_raise RuntimeError, "decode", fn -> 133 | query("SELECT true", []) 134 | end 135 | 136 | pid = context[:pid] 137 | assert_receive {:EXIT, ^pid, :killed} 138 | end) =~ "(RuntimeError) decode" 139 | end 140 | 141 | test "execute prepared query on connection with different types", context do 142 | query = prepare("S42", "SELECT 42") 143 | 144 | opts = [types: Postgrex.DefaultTypes] ++ context[:options] 145 | {:ok, pid2} = Postgrex.start_link(opts) 146 | 147 | {:ok, %Postgrex.Query{}, %Result{rows: [[42]]}} = Postgrex.execute(pid2, query, []) 148 | end 149 | 150 | test "stream prepared query on connection with different types", context do 151 | query = prepare("S42", "SELECT 42") 152 | 153 | opts = [types: Postgrex.DefaultTypes] ++ context[:options] 154 | {:ok, pid2} = Postgrex.start_link(opts) 155 | 156 | Postgrex.transaction(pid2, fn conn -> 157 | assert [%Result{rows: [[42]]}] = stream(query, []) |> Enum.take(1) 158 | end) 159 | end 160 | 161 | test "stream prepared COPY FROM on connection with different types", context do 162 | query = prepare("copy", "COPY uniques FROM STDIN") 163 | 164 | opts = [types: Postgrex.DefaultTypes] ++ context[:options] 165 | {:ok, pid2} = Postgrex.start_link(opts) 166 | 167 | Postgrex.transaction(pid2, fn conn -> 168 | stream = stream(query, []) 169 | assert Enum.into(["1\n"], stream) == stream 170 | Postgrex.rollback(conn, :done) 171 | end) 172 | end 173 | 174 | test "dont decode text format", context do 175 | assert [["123.45"]] = query("SELECT 123.45::float8", []) 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /test/error_code_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ErrorCodeTest do 2 | use ExUnit.Case, async: true 3 | import Postgrex.ErrorCode 4 | 5 | doctest Postgrex.ErrorCode 6 | 7 | test "code to name" do 8 | assert code_to_name("23505") == :unique_violation 9 | assert code_to_name("2F003") == :prohibited_sql_statement_attempted 10 | assert code_to_name("38003") == :prohibited_sql_statement_attempted 11 | assert code_to_name("nope") == nil 12 | end 13 | 14 | test "name to codes" do 15 | assert name_to_code(:unique_violation) == "23505" 16 | assert name_to_code(:prohibited_sql_statement_attempted) == "2F003" 17 | assert catch_error(name_to_code(:nope)) 18 | end 19 | 20 | test "new codes from 9.5" do 21 | # https://github.com/postgres/postgres/commit/73206812cd97436cffd8f331dbb09d38a2728162 22 | assert code_to_name("39P03") == :event_trigger_protocol_violated 23 | assert name_to_code(:event_trigger_protocol_violated) == "39P03" 24 | 25 | # https://github.com/postgres/postgres/commit/a4847fc3ef139ba9a8ffebb6ffa06ee72078ffa2 26 | assert name_to_code(:assert_failure) == "P0004" 27 | assert code_to_name("P0004") == :assert_failure 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ErrorTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Postgrex, as: P 5 | 6 | setup do 7 | opts = [database: "postgrex_test", backoff_type: :stop] 8 | {:ok, pid} = P.start_link(opts) 9 | {:ok, pid: pid} 10 | end 11 | 12 | @tag min_pg_version: "9.3" 13 | test "encodes code, detail, table and constraint", config do 14 | {:error, error} = P.query(config.pid, "insert into uniques values (1), (1);", []) 15 | message = Exception.message(error) 16 | assert message =~ "duplicate key value violates unique constraint" 17 | assert message =~ "table: uniques" 18 | assert message =~ "constraint: uniques_a_key" 19 | assert message =~ "ERROR 23505" 20 | end 21 | 22 | @tag min_pg_version: "9.3" 23 | test "encodes custom hint", config do 24 | query = """ 25 | DO language plpgsql $$ BEGIN 26 | RAISE EXCEPTION 'oops' USING HINT = 'custom hint'; 27 | END; 28 | $$; 29 | """ 30 | 31 | {:error, error} = P.query(config.pid, query, []) 32 | message = Exception.message(error) 33 | assert message =~ "oops" 34 | assert message =~ "hint: custom hint" 35 | end 36 | 37 | @tag min_pg_version: "9.3" 38 | test "includes query on invalid syntax", config do 39 | {:error, error} = P.query(config.pid, "SELCT true;", []) 40 | message = Exception.message(error) 41 | assert message =~ "ERROR 42601 (syntax_error) syntax error at or near \"SELCT\"" 42 | assert message =~ "query: SELCT true" 43 | end 44 | 45 | @tag min_pg_version: "9.1" 46 | test "notices during execute", config do 47 | {:ok, _} = P.query(config.pid, "CREATE TABLE IF NOT EXISTS notices (id int)", []) 48 | {:ok, result} = P.query(config.pid, "CREATE TABLE IF NOT EXISTS notices (id int)", []) 49 | assert [%{message: "relation \"notices\" already exists, skipping"}] = result.messages 50 | end 51 | 52 | @tag min_pg_version: "9.1" 53 | test "notices during prepare", config do 54 | {:ok, _} = P.query(config.pid, "CREATE TABLE IF NOT EXISTS notices (id int)", []) 55 | 56 | {:ok, result} = 57 | P.query( 58 | config.pid, 59 | "ALTER TABLE notices ADD CONSTRAINT " <> 60 | "my_very_very_very_very_long_and_very_explicit_and_even_longer_fkey CHECK (id < 5)", 61 | [] 62 | ) 63 | 64 | assert [%{message: _, code: "42622"}] = result.messages 65 | end 66 | 67 | test "notices raised by functions do not reset rows", config do 68 | {:ok, _} = 69 | P.query( 70 | config.pid, 71 | """ 72 | CREATE FUNCTION raise_notice_and_return(what integer) RETURNS integer AS $$ 73 | BEGIN 74 | RAISE NOTICE 'notice %', what; 75 | RETURN what; 76 | END; 77 | $$ LANGUAGE plpgsql; 78 | """, 79 | [] 80 | ) 81 | 82 | assert {:ok, result} = 83 | P.query( 84 | config.pid, 85 | "SELECT raise_notice_and_return(x) FROM generate_series(1, 2) AS x", 86 | [] 87 | ) 88 | 89 | assert [_, _] = result.messages 90 | assert [[1], [2]] = result.rows 91 | end 92 | 93 | test "errors raised by functions disconnect", config do 94 | {:ok, _} = 95 | P.query( 96 | config.pid, 97 | """ 98 | CREATE FUNCTION raise_exception(what integer) RETURNS integer AS $$ 99 | BEGIN 100 | RAISE EXCEPTION 'error %', what; 101 | END; 102 | $$ LANGUAGE plpgsql; 103 | """, 104 | [] 105 | ) 106 | 107 | assert {:error, %Postgrex.Error{postgres: %{message: "error 1"}}} = 108 | P.query(config.pid, "SELECT raise_exception(1)", []) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/notification_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NotificationTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Postgrex.TestHelper 5 | 6 | alias Postgrex, as: P 7 | alias Postgrex.Notifications, as: PN 8 | 9 | @opts [database: "postgrex_test", sync_connect: true, reconnect_backoff: 250] 10 | 11 | setup do 12 | {:ok, pid} = P.start_link(@opts) 13 | {:ok, pid_ps} = PN.start_link(@opts) 14 | {:ok, [pid: pid, pid_ps: pid_ps]} 15 | end 16 | 17 | test "defines child spec" do 18 | start_supervised!({PN, @opts}) 19 | end 20 | 21 | test "fails on sync connection by default" do 22 | Process.flag(:trap_exit, true) 23 | assert {:error, _} = PN.start_link(database: "nobody_knows_it") 24 | end 25 | 26 | test "does not fail on sync connection with auto reconnect" do 27 | Process.flag(:trap_exit, true) 28 | assert {:ok, pid} = PN.start_link(database: "nobody_knows_it", auto_reconnect: true) 29 | assert {:eventually, _} = PN.listen(pid, "channel") 30 | end 31 | 32 | test "does not fail on async connection with auto reconnect" do 33 | Process.flag(:trap_exit, true) 34 | 35 | assert {:ok, pid} = 36 | PN.start_link(database: "nobody_knows_it", auto_reconnect: true, sync_connect: false) 37 | 38 | assert {:eventually, _} = PN.listen(pid, "channel") 39 | refute_receive {:EXIT, _, ^pid}, 100 40 | end 41 | 42 | test "listening", context do 43 | assert {:ok, ref} = PN.listen(context.pid_ps, "channel") 44 | 45 | assert is_reference(ref) 46 | end 47 | 48 | test "notifying", context do 49 | assert :ok = query("NOTIFY channel", []) 50 | end 51 | 52 | @tag requires_notify_payload: true 53 | test "listening, notify, then receive (with payload)", context do 54 | assert {:ok, ref} = PN.listen(context.pid_ps, "channel") 55 | 56 | assert {:ok, %Postgrex.Result{command: :notify}} = 57 | P.query(context.pid, "NOTIFY channel, 'hello'", []) 58 | 59 | receiver_pid = context.pid_ps 60 | assert_receive {:notification, ^receiver_pid, ^ref, "channel", "hello"} 61 | end 62 | 63 | test "listening, notify, then receive (without payload)", context do 64 | assert {:ok, ref} = PN.listen(context.pid_ps, "channel") 65 | 66 | assert {:ok, %Postgrex.Result{command: :notify}} = P.query(context.pid, "NOTIFY channel", []) 67 | receiver_pid = context.pid_ps 68 | assert_receive {:notification, ^receiver_pid, ^ref, "channel", ""} 69 | end 70 | 71 | test "listening, notify, then receive (using registered names)", _context do 72 | {:ok, _} = P.start_link(Keyword.put(@opts, :name, :client)) 73 | {:ok, _pn} = PN.start_link(Keyword.put(@opts, :name, :notifications)) 74 | assert {:ok, ref} = PN.listen(:notifications, "channel") 75 | 76 | assert {:ok, %Postgrex.Result{command: :notify}} = P.query(:client, "NOTIFY channel", []) 77 | receiver_pid = Process.whereis(:notifications) 78 | assert_receive {:notification, ^receiver_pid, ^ref, "channel", ""} 79 | end 80 | 81 | test "listening, unlistening, notify, don't receive", context do 82 | assert {:ok, ref} = PN.listen(context.pid_ps, "channel") 83 | assert :ok = PN.unlisten(context.pid_ps, ref) 84 | 85 | assert {:ok, %Postgrex.Result{command: :notify}} = P.query(context.pid, "NOTIFY channel", []) 86 | pid = context.pid_ps 87 | refute_receive {:notification, ^pid, ^ref, "channel", ""} 88 | end 89 | 90 | test "listening x2, unlistening, notify, receive", context do 91 | {:ok, other_pid_ps} = PN.start_link(@opts) 92 | 93 | assert {:ok, ref1} = PN.listen(context.pid_ps, "channel") 94 | assert {:ok, ref2} = PN.listen(other_pid_ps, "channel") 95 | 96 | assert :ok = PN.unlisten(other_pid_ps, ref2) 97 | assert {:ok, %Postgrex.Result{command: :notify}} = P.query(context.pid, "NOTIFY channel", []) 98 | 99 | pid = context.pid_ps 100 | assert_receive {:notification, ^pid, ^ref1, "channel", ""}, 1_000 101 | end 102 | 103 | test "listen, go away", context do 104 | spawn(fn -> 105 | assert {:ok, _} = PN.listen(context.pid_ps, "channel") 106 | end) 107 | 108 | assert {:ok, %Postgrex.Result{command: :notify}} = P.query(context.pid, "NOTIFY channel", []) 109 | 110 | Process.sleep(300) 111 | end 112 | 113 | describe "reconnection" do 114 | setup do 115 | {:ok, pid_ps} = PN.start_link(Keyword.put(@opts, :auto_reconnect, true)) 116 | {:ok, pid_ps: pid_ps} 117 | end 118 | 119 | test "basic reconnection", context do 120 | assert {:ok, ref} = PN.listen(context.pid_ps, "channel") 121 | 122 | disconnect(context.pid_ps) 123 | 124 | # Give the notifier a chance to re-establish the connection and listeners 125 | Process.sleep(500) 126 | 127 | assert {:ok, %Postgrex.Result{command: :notify}} = 128 | P.query(context.pid, "NOTIFY channel", []) 129 | 130 | receiver_pid = context.pid_ps 131 | assert_receive {:notification, ^receiver_pid, ^ref, "channel", ""} 132 | end 133 | 134 | test "reestablish multiple listeners and channels", context do 135 | receiver_pid = context.pid_ps 136 | 137 | assert {:ok, ref} = PN.listen(context.pid_ps, "channel") 138 | 139 | async = 140 | Task.async(fn -> 141 | assert {:ok, ref3} = PN.listen(context.pid_ps, "channel") 142 | assert_receive {:notification, ^receiver_pid, ^ref3, "channel", ""} 143 | end) 144 | 145 | disconnect(context.pid_ps) 146 | 147 | # Also attempt to subscribe while it is down 148 | assert {ok_or_eventually, ref2} = PN.listen(context.pid_ps, "channel2") 149 | assert ok_or_eventually in [:ok, :eventually] 150 | 151 | # Give the notifier a chance to re-establish the connection and listeners 152 | Process.sleep(500) 153 | 154 | assert {:ok, %Postgrex.Result{command: :notify}} = 155 | P.query(context.pid, "NOTIFY channel", []) 156 | 157 | assert_receive {:notification, ^receiver_pid, ^ref, "channel", ""} 158 | 159 | assert {:ok, %Postgrex.Result{command: :notify}} = 160 | P.query(context.pid, "NOTIFY channel2", []) 161 | 162 | assert_receive {:notification, ^receiver_pid, ^ref2, "channel2", ""} 163 | 164 | assert Task.await(async) 165 | end 166 | end 167 | 168 | test "dynamic configuration with named function" do 169 | {:ok, _} = 170 | PN.start_link( 171 | database: "nobody_knows_it", 172 | configure: {NotificationTest, :configure, [self(), :bar, :baz]}, 173 | foo: :bar 174 | ) 175 | 176 | assert_received :configured 177 | end 178 | 179 | test "dynamic configuration with anonymous function" do 180 | {:ok, _} = 181 | PN.start_link( 182 | database: "nobody_knows_it", 183 | configure: fn opts -> 184 | assert :bar = Keyword.get(opts, :foo) 185 | send(opts[:parent], :configured) 186 | Keyword.merge(opts, @opts) 187 | end, 188 | foo: :bar, 189 | parent: self() 190 | ) 191 | 192 | assert_received :configured 193 | end 194 | 195 | def configure(opts, parent, :bar, :baz) do 196 | assert :bar = Keyword.get(opts, :foo) 197 | send(parent, :configured) 198 | Keyword.merge(opts, @opts) 199 | end 200 | 201 | defp disconnect(conn) do 202 | {_, state} = :sys.get_state(conn) 203 | {:gen_tcp, sock} = state.protocol.sock 204 | :gen_tcp.shutdown(sock, :read_write) 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /test/postgrex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PostgrexTest do 2 | use ExUnit.Case, async: false 3 | import ExUnit.CaptureLog, warn: false 4 | 5 | test "start_link/2 sets search path" do 6 | search_path = ["public", "extension"] 7 | {:ok, conn} = Postgrex.start_link(database: "postgrex_test", search_path: search_path) 8 | %{rows: [[result]]} = Postgrex.query!(conn, "show search_path", []) 9 | 10 | assert result == Enum.join(search_path, ", ") 11 | end 12 | 13 | # gen_statem reports are only captured on Elixir v1.17+ 14 | # but a bug causes the Logger to crash on v1.17.0 and v1.17.1. 15 | if Version.match?(System.version(), ">= 1.17.2") do 16 | test "start_link/2 detects invalid search path" do 17 | # invalid argument 18 | Process.flag(:trap_exit, true) 19 | search_path = "public, extension" 20 | 21 | opts = [ 22 | database: "postgrex_test", 23 | search_path: search_path, 24 | show_sensitive_data_on_connection_error: true 25 | ] 26 | 27 | error_log = 28 | capture_log(fn -> 29 | Postgrex.start_link(opts) 30 | assert_receive {:EXIT, _, :killed} 31 | end) 32 | 33 | assert error_log =~ "expected :search_path to be a list of strings, got: \"#{search_path}\"" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/schema_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SchemaTest do 2 | use ExUnit.Case, async: true 3 | import Postgrex.TestHelper 4 | alias Postgrex, as: P 5 | 6 | setup do 7 | opts = [database: "postgrex_test_with_schemas"] 8 | {:ok, pid} = P.start_link(opts) 9 | {:ok, [pid: pid]} 10 | end 11 | 12 | @tag min_pg_version: "9.1" 13 | test "encode hstore", context do 14 | assert [ 15 | [ 16 | %{ 17 | "name" => "Frank", 18 | "bubbles" => "7", 19 | "limit" => nil, 20 | "chillin" => "true", 21 | "fratty" => "false", 22 | "atom" => "bomb" 23 | } 24 | ] 25 | ] = 26 | query(~s(SELECT $1::test.hstore), [ 27 | %{ 28 | "name" => "Frank", 29 | "bubbles" => "7", 30 | "limit" => nil, 31 | "chillin" => "true", 32 | "fratty" => "false", 33 | "atom" => "bomb" 34 | } 35 | ]) 36 | end 37 | 38 | @tag min_pg_version: "9.1" 39 | test "decode hstore inside a schema", context do 40 | assert [[%{}]] = query(~s{SELECT ''::test.hstore}, []) 41 | 42 | assert [[%{"Bubbles" => "7", "Name" => "Frank"}]] = 43 | query(~s{SELECT '"Name" => "Frank", "Bubbles" => "7"'::test.hstore}, []) 44 | 45 | assert [[%{"non_existant" => nil, "present" => "&accounted_for"}]] = 46 | query( 47 | ~s{SELECT '"non_existant" => NULL, "present" => "&accounted_for"'::test.hstore}, 48 | [] 49 | ) 50 | 51 | assert [[%{"spaces in the key" => "are easy!", "floats too" => "66.6"}]] = 52 | query( 53 | ~s{SELECT '"spaces in the key" => "are easy!", "floats too" => "66.6"'::test.hstore}, 54 | [] 55 | ) 56 | 57 | assert [[%{"this is true" => "true", "though not this" => "false"}]] = 58 | query( 59 | ~s{SELECT '"this is true" => "true", "though not this" => "false"'::test.hstore}, 60 | [] 61 | ) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/scram/locked_cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PostGrex.SCRAM.LockedCacheTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Postgrex.SCRAM.LockedCache, as: LC 5 | 6 | test "caches keys", config do 7 | assert LC.run(config.test, fn -> :this_is_cached end) == :this_is_cached 8 | assert LC.run(config.test, fn -> flunk() end) == :this_is_cached 9 | end 10 | 11 | test "locks cache keys", config do 12 | %Task{pid: first_pid, ref: first_ref} = task_run(config.test, fn -> :this_is_cached end) 13 | 14 | assert_receive {:running, ^first_pid} 15 | 16 | %Task{pid: second_pid, ref: second_ref} = task_run(config.test, fn -> flunk() end) 17 | send(first_pid, :run) 18 | 19 | assert_receive {^first_ref, :this_is_cached} 20 | assert_receive {^second_ref, :this_is_cached} 21 | assert_receive {:DOWN, ^first_ref, _, _, _} 22 | assert_receive {:DOWN, ^second_ref, _, _, _} 23 | refute_received {:running, ^second_pid} 24 | end 25 | 26 | test "allows cache to be recomputed if cache fails", config do 27 | assert catch_error(LC.run(config.test, fn -> raise "oops" end)) 28 | assert LC.run(config.test, fn -> :this_is_cached end) == :this_is_cached 29 | end 30 | 31 | @tag :capture_log 32 | test "allows cache to be recomputed if cache fails when locked", config do 33 | Process.flag(:trap_exit, true) 34 | %Task{pid: error_pid, ref: error_ref} = task_run(config.test, fn -> raise "oops" end) 35 | assert_receive {:running, ^error_pid} 36 | 37 | %Task{pid: ok_pid} = ok_task = task_run(config.test, fn -> :this_is_cached end) 38 | refute_received {:running, ^ok_pid} 39 | 40 | send(error_pid, :run) 41 | assert_receive {:DOWN, ^error_ref, _, _, _} 42 | assert_receive {:running, ^ok_pid} 43 | 44 | send(ok_pid, :run) 45 | assert Task.await(ok_task) == :this_is_cached 46 | assert LC.run(config.test, fn -> flunk() end) == :this_is_cached 47 | end 48 | 49 | @tag :capture_log 50 | test "allows cache to be recomputed if cache exits", config do 51 | Process.flag(:trap_exit, true) 52 | %Task{pid: error_pid, ref: error_ref} = task_run(config.test, fn -> raise "oops" end) 53 | assert_receive {:running, ^error_pid} 54 | Process.exit(error_pid, :kill) 55 | assert_receive {:DOWN, ^error_ref, _, _, :killed} 56 | 57 | assert LC.run(config.test, fn -> :this_is_cached end) == :this_is_cached 58 | end 59 | 60 | @tag :capture_log 61 | test "allows cache to be recomputed if cache exits when locked", config do 62 | Process.flag(:trap_exit, true) 63 | %Task{pid: error_pid, ref: error_ref} = task_run(config.test, fn -> raise "oops" end) 64 | assert_receive {:running, ^error_pid} 65 | 66 | %Task{pid: ok_pid} = ok_task = task_run(config.test, fn -> :this_is_cached end) 67 | refute_received {:running, ^ok_pid} 68 | 69 | Process.exit(error_pid, :kill) 70 | assert_receive {:DOWN, ^error_ref, _, _, :killed} 71 | assert_receive {:running, ^ok_pid} 72 | 73 | send(ok_pid, :run) 74 | assert Task.await(ok_task) == :this_is_cached 75 | assert LC.run(config.test, fn -> flunk() end) == :this_is_cached 76 | end 77 | 78 | defp task_run(key, fun) do 79 | parent = self() 80 | 81 | Task.async(fn -> 82 | LC.run(key, fn -> 83 | send(parent, {:running, self()}) 84 | assert_receive :run 85 | fun.() 86 | end) 87 | end) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/simple_connection_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleConnectionTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Postgrex.SimpleConnection, as: SC 5 | 6 | defmodule Conn do 7 | @behaviour Postgrex.SimpleConnection 8 | 9 | @impl true 10 | def init(pid) do 11 | {:ok, %{from: nil, pid: pid}} 12 | end 13 | 14 | @impl true 15 | def notify(channel, payload, state) do 16 | send(state.pid, {channel, payload}) 17 | 18 | :ok 19 | end 20 | 21 | @impl true 22 | def handle_connect(state) do 23 | state.from && Postgrex.SimpleConnection.reply(state.from, :reconnecting) 24 | 25 | send(state.pid, {:connect, System.unique_integer([:monotonic])}) 26 | 27 | {:noreply, state} 28 | end 29 | 30 | @impl true 31 | def handle_disconnect(state) do 32 | send(state.pid, {:disconnect, System.unique_integer([:monotonic])}) 33 | 34 | {:noreply, state} 35 | end 36 | 37 | @impl true 38 | def handle_call(:ping, from, state) do 39 | Postgrex.SimpleConnection.reply(from, :pong) 40 | 41 | {:noreply, state} 42 | end 43 | 44 | def handle_call({:query, query}, from, state) do 45 | {:query, query, %{state | from: from}} 46 | end 47 | 48 | @impl true 49 | def handle_info(:ping, state) do 50 | send(state.pid, :pong) 51 | 52 | {:noreply, state} 53 | end 54 | 55 | @impl true 56 | def handle_result(result, %{from: from} = state) do 57 | Postgrex.SimpleConnection.reply(from, {:ok, result}) 58 | 59 | {:noreply, state} 60 | end 61 | end 62 | 63 | @opts [database: "postgrex_test", sync_connect: true, auto_reconnect: false] 64 | 65 | setup context do 66 | opts = Keyword.merge(@opts, context[:opts] || []) 67 | conn = start_supervised!({SC, [Conn, self(), opts]}) 68 | 69 | {:ok, conn: conn} 70 | end 71 | 72 | describe "handle_call/3" do 73 | test "forwarding calls to the callback module", context do 74 | assert :pong == SC.call(context.conn, :ping) 75 | end 76 | end 77 | 78 | describe "handle_info/2" do 79 | test "forwarding unknown messages to the callback module", context do 80 | send(context.conn, :ping) 81 | 82 | assert_receive :pong 83 | end 84 | end 85 | 86 | describe "handle_result/2" do 87 | test "relaying query results", context do 88 | assert {:ok, [%Postgrex.Result{}]} = SC.call(context.conn, {:query, "SELECT 1"}) 89 | end 90 | 91 | test "relaying multi-statement query results", context do 92 | assert {:ok, [%Postgrex.Result{} = result1, %Postgrex.Result{} = result2]} = 93 | SC.call(context.conn, {:query, "SELECT 1; SELECT 2;"}) 94 | 95 | assert result1.rows == [["1"]] 96 | assert result1.num_rows == 1 97 | assert result2.rows == [["2"]] 98 | assert result2.num_rows == 1 99 | end 100 | 101 | test "relaying query errors", context do 102 | assert {:ok, %Postgrex.Error{}} = SC.call(context.conn, {:query, "SELCT"}) 103 | end 104 | end 105 | 106 | describe "notify/3" do 107 | test "relaying pubsub notifications", context do 108 | assert {:ok, _result} = SC.call(context.conn, {:query, ~s(LISTEN "channel")}) 109 | assert {:ok, _result} = SC.call(context.conn, {:query, ~s(NOTIFY "channel", 'hello')}) 110 | 111 | assert_receive {"channel", "hello"} 112 | end 113 | end 114 | 115 | describe "auto-reconnect" do 116 | @tag opts: [auto_reconnect: true] 117 | test "disconnect and connect handlers are invoked on reconnection", context do 118 | assert_receive {:connect, i1} 119 | 120 | :sys.suspend(context.conn) 121 | 122 | :erlang.trace(:all, true, [:call]) 123 | match_spec = [{:_, [], [{:return_trace}]}] 124 | :erlang.trace_pattern({SC, :_, :_}, match_spec, [:local]) 125 | 126 | task = 127 | Task.async(fn -> 128 | SC.call(context.conn, {:query, "SELECT 1"}) 129 | end) 130 | 131 | # Make sure that the task "launched" the call before disconnecting and resuming 132 | # the process. 133 | assert_receive {:trace, _pid, :call, {SC, :call, [_, {:query, "SELECT 1"}]}} 134 | 135 | disconnect(context.conn) 136 | :sys.resume(context.conn) 137 | 138 | assert {:ok, [%Postgrex.Result{}]} = SC.call(context.conn, {:query, "SELECT 2"}) 139 | assert :reconnecting == Task.await(task) 140 | assert_receive {:disconnect, i2} when i1 < i2 141 | assert_receive {:connect, i3} when i2 < i3 142 | after 143 | :erlang.trace_pattern({SC, :_, :_}, false, []) 144 | :erlang.trace(:all, false, [:call]) 145 | end 146 | 147 | @tag capture_log: true 148 | test "disconnect handler is invoked and the process stays down", context do 149 | assert_receive {:connect, _} 150 | 151 | Process.flag(:trap_exit, true) 152 | 153 | :sys.suspend(context.conn) 154 | {_pid, ref} = spawn_monitor(fn -> SC.call(context.conn, {:query, "SELECT 1"}) end) 155 | disconnect(context.conn) 156 | :sys.resume(context.conn) 157 | 158 | assert_receive {:DOWN, ^ref, _, _, _} 159 | assert_received {:disconnect, _} 160 | 161 | ref = Process.monitor(context.conn) 162 | assert_receive {:DOWN, ^ref, _, _, _} 163 | 164 | refute_received {:connect, _} 165 | end 166 | end 167 | 168 | defp disconnect(conn) do 169 | {_, state} = :sys.get_state(conn) 170 | {:gen_tcp, sock} = state.protocol.sock 171 | :gen_tcp.shutdown(sock, :read_write) 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | defmodule PSQL do 2 | @pg_env %{"PGUSER" => System.get_env("PGUSER") || "postgres"} 3 | 4 | def cmd(args) do 5 | {output, status} = System.cmd("psql", args, stderr_to_stdout: true, env: @pg_env) 6 | 7 | if status != 0 do 8 | IO.puts(""" 9 | Command: 10 | 11 | psql #{Enum.join(args, " ")} 12 | 13 | error'd with: 14 | 15 | #{output} 16 | 17 | Please verify the user "postgres" exists and it has permissions to 18 | create databases and users. If not, you can create a new user with: 19 | 20 | $ createuser postgres -s --no-password 21 | """) 22 | 23 | System.halt(1) 24 | end 25 | 26 | output 27 | end 28 | 29 | def vsn do 30 | vsn_select = cmd(["-c", "SELECT version();"]) 31 | [_, major, minor] = Regex.run(~r/PostgreSQL (\d+).(\d+)/, vsn_select) 32 | {String.to_integer(major), String.to_integer(minor)} 33 | end 34 | 35 | def supports_sockets? do 36 | otp_release = :otp_release |> :erlang.system_info() |> List.to_integer() 37 | unix_socket_dir = System.get_env("PG_SOCKET_DIR") || "/tmp" 38 | port = System.get_env("PGPORT") || "5432" 39 | unix_socket_path = Path.join(unix_socket_dir, ".s.PGSQL.#{port}") 40 | otp_release >= 20 and File.exists?(unix_socket_path) 41 | end 42 | 43 | def supports_ssl? do 44 | cmd(["-c", "SHOW ssl"]) =~ "on" 45 | end 46 | 47 | def supports_logical_replication? do 48 | cmd(["-c", "SHOW wal_level"]) =~ "logical" 49 | end 50 | end 51 | 52 | pg_version = PSQL.vsn() 53 | unix_exclude = if PSQL.supports_sockets?(), do: [], else: [unix: true] 54 | ssl_exclude = if PSQL.supports_ssl?(), do: [], else: [ssl: true] 55 | notify_exclude = if pg_version == {8, 4}, do: [requires_notify_payload: true], else: [] 56 | 57 | replication_exclude = 58 | if pg_version < {10, 0} or PSQL.supports_logical_replication?() do 59 | [] 60 | else 61 | IO.puts(:stderr, """ 62 | !!! Skipping replication tests because wal_level is not set to logical. 63 | 64 | To run them, you must run the following commands and restart your database: 65 | 66 | ALTER SYSTEM SET wal_level='logical'; 67 | ALTER SYSTEM SET max_wal_senders='10'; 68 | ALTER SYSTEM SET max_replication_slots='10'; 69 | """) 70 | 71 | [logical_replication: true] 72 | end 73 | 74 | version_exclude = 75 | [{8, 4}, {9, 0}, {9, 1}, {9, 2}, {9, 3}, {9, 4}, {9, 5}, {10, 0}, {13, 0}, {14, 0}] 76 | |> Enum.filter(fn x -> x > pg_version end) 77 | |> Enum.map(fn {major, minor} -> {:min_pg_version, "#{major}.#{minor}"} end) 78 | 79 | excludes = version_exclude ++ replication_exclude ++ notify_exclude ++ unix_exclude ++ ssl_exclude 80 | ExUnit.start(exclude: excludes, assert_receive_timeout: 1000) 81 | 82 | sql_test = """ 83 | DROP ROLE IF EXISTS postgrex_cleartext_pw; 84 | DROP ROLE IF EXISTS postgrex_md5_pw; 85 | 86 | CREATE USER postgrex_cleartext_pw WITH PASSWORD 'postgrex_cleartext_pw'; 87 | CREATE USER postgrex_md5_pw WITH PASSWORD 'postgrex_md5_pw'; 88 | 89 | DROP TABLE IF EXISTS composite1; 90 | CREATE TABLE composite1 (a int, b text); 91 | 92 | DROP TABLE IF EXISTS composite2; 93 | CREATE TABLE composite2 (a int, b int, c int); 94 | 95 | DROP TYPE IF EXISTS enum1; 96 | CREATE TYPE enum1 AS ENUM ('elixir', 'erlang'); 97 | 98 | CREATE TABLE uniques (a int UNIQUE); 99 | CREATE TABLE timestamps (micro timestamp, milli timestamp(3), sec timestamp(0), sec_arr timestamp(0)[]); 100 | CREATE TABLE timestamps_stream (micro timestamp, milli timestamp(3), sec timestamp(0), sec_arr timestamp(0)[]); 101 | 102 | DROP TABLE IF EXISTS missing_oid; 103 | DROP TYPE IF EXISTS missing_enum; 104 | DROP TYPE IF EXISTS missing_comp; 105 | 106 | CREATE TABLE altering (a int2); 107 | 108 | CREATE TABLE calendar (a timestamp without time zone, b timestamp with time zone); 109 | 110 | DROP DOMAIN IF EXISTS points_domain; 111 | CREATE DOMAIN points_domain AS point[] CONSTRAINT is_populated CHECK (COALESCE(array_length(VALUE, 1), 0) >= 1); 112 | 113 | DROP DOMAIN IF EXISTS floats_domain; 114 | CREATE DOMAIN floats_domain AS float[] CONSTRAINT is_populated CHECK (COALESCE(array_length(VALUE, 1), 0) >= 1); 115 | """ 116 | 117 | sql_test = 118 | if pg_version >= {10, 0} do 119 | sql_test <> 120 | """ 121 | DROP ROLE IF EXISTS postgrex_scram_pw; 122 | SET password_encryption = 'scram-sha-256'; 123 | CREATE USER postgrex_scram_pw WITH PASSWORD 'postgrex_scram_pw'; 124 | CREATE PUBLICATION postgrex_example FOR ALL TABLES; 125 | """ 126 | else 127 | sql_test 128 | end 129 | 130 | sql_test_with_schemas = """ 131 | DROP SCHEMA IF EXISTS test; 132 | CREATE SCHEMA test; 133 | """ 134 | 135 | PSQL.cmd(["-c", "DROP DATABASE IF EXISTS postgrex_test;"]) 136 | PSQL.cmd(["-c", "DROP DATABASE IF EXISTS postgrex_test_with_schemas;"]) 137 | 138 | PSQL.cmd([ 139 | "-c", 140 | "CREATE DATABASE postgrex_test TEMPLATE=template0 ENCODING='UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8';" 141 | ]) 142 | 143 | PSQL.cmd([ 144 | "-c", 145 | "CREATE DATABASE postgrex_test_with_schemas TEMPLATE=template0 ENCODING='UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8';" 146 | ]) 147 | 148 | PSQL.cmd(["-d", "postgrex_test", "-c", sql_test]) 149 | PSQL.cmd(["-d", "postgrex_test_with_schemas", "-c", sql_test_with_schemas]) 150 | 151 | cond do 152 | pg_version >= {9, 1} -> 153 | PSQL.cmd([ 154 | "-d", 155 | "postgrex_test_with_schemas", 156 | "-c", 157 | "CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA test;" 158 | ]) 159 | 160 | PSQL.cmd(["-d", "postgrex_test", "-c", "CREATE EXTENSION IF NOT EXISTS hstore;"]) 161 | 162 | pg_version == {9, 0} -> 163 | pg_path = System.get_env("PGPATH") 164 | PSQL.cmd(["-d", "postgrex_test", "-f", "#{pg_path}/contrib/hstore.sql"]) 165 | 166 | pg_version < {9, 0} -> 167 | PSQL.cmd(["-d", "postgrex_test", "-c", "CREATE LANGUAGE plpgsql;"]) 168 | 169 | true -> 170 | :ok 171 | end 172 | 173 | PSQL.cmd(["-d", "postgrex_test", "-c", "CREATE EXTENSION IF NOT EXISTS ltree;"]) 174 | 175 | defmodule Postgrex.TestHelper do 176 | defmacro query(stat, params, opts \\ []) do 177 | quote do 178 | case Postgrex.query(var!(context)[:pid], unquote(stat), unquote(params), unquote(opts)) do 179 | {:ok, %Postgrex.Result{rows: nil}} -> :ok 180 | {:ok, %Postgrex.Result{rows: rows}} -> rows 181 | {:error, err} -> err 182 | end 183 | end 184 | end 185 | 186 | defmacro prepare(name, stat, opts \\ []) do 187 | quote do 188 | case Postgrex.prepare(var!(context)[:pid], unquote(name), unquote(stat), unquote(opts)) do 189 | {:ok, %Postgrex.Query{} = query} -> query 190 | {:error, err} -> err 191 | end 192 | end 193 | end 194 | 195 | defmacro prepare_execute(name, stat, params, opts \\ []) do 196 | quote do 197 | case Postgrex.prepare_execute( 198 | var!(context)[:pid], 199 | unquote(name), 200 | unquote(stat), 201 | unquote(params), 202 | unquote(opts) 203 | ) do 204 | {:ok, %Postgrex.Query{} = query, %Postgrex.Result{rows: rows}} -> {query, rows} 205 | {:error, err} -> err 206 | end 207 | end 208 | end 209 | 210 | defmacro execute(query, params, opts \\ []) do 211 | quote do 212 | case Postgrex.execute(var!(context)[:pid], unquote(query), unquote(params), unquote(opts)) do 213 | {:ok, %Postgrex.Query{}, %Postgrex.Result{rows: nil}} -> :ok 214 | {:ok, %Postgrex.Query{}, %Postgrex.Result{rows: rows}} -> rows 215 | {:error, err} -> err 216 | end 217 | end 218 | end 219 | 220 | defmacro stream(query, params, opts \\ []) do 221 | quote do 222 | Postgrex.stream(var!(conn), unquote(query), unquote(params), unquote(opts)) 223 | end 224 | end 225 | 226 | defmacro close(query, opts \\ []) do 227 | quote do 228 | case Postgrex.close(var!(context)[:pid], unquote(query), unquote(opts)) do 229 | :ok -> :ok 230 | {:error, err} -> err 231 | end 232 | end 233 | end 234 | 235 | defmacro transaction(fun, opts \\ []) do 236 | quote do 237 | Postgrex.transaction(var!(context)[:pid], unquote(fun), unquote(opts)) 238 | end 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /test/tsvector_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TsvectorTest do 2 | use ExUnit.Case, async: true 3 | import Postgrex.TestHelper 4 | alias Postgrex, as: P 5 | alias Postgrex.Lexeme 6 | 7 | setup do 8 | opts = [database: "postgrex_test", backoff_type: :stop] 9 | {:ok, pid} = P.start_link(opts) 10 | {:ok, [pid: pid]} 11 | end 12 | 13 | test "encode basic tsvector", context do 14 | assert [["'1'"]] = query("SELECT $1::tsvector::text", [[%Lexeme{positions: [], word: "1"}]]) 15 | 16 | assert [["'1' 'hello'"]] = 17 | query("SELECT $1::tsvector::text", [ 18 | [%Lexeme{positions: [], word: "1"}, %Lexeme{positions: [], word: "hello"}] 19 | ]) 20 | end 21 | 22 | test "encode tsvector with positions", context do 23 | assert [["'1':1"]] = 24 | query("SELECT $1::tsvector::text", [[%Lexeme{positions: [{1, nil}], word: "1"}]]) 25 | end 26 | 27 | test "encode tsvector with multiple positions", context do 28 | assert [["'1':1,2"]] = 29 | query("SELECT $1::tsvector::text", [ 30 | [%Lexeme{positions: [{1, nil}, {2, nil}], word: "1"}] 31 | ]) 32 | end 33 | 34 | test "encode tsvector with position and weight", context do 35 | assert [["'car':1A"]] = 36 | query("SELECT $1::tsvector::text", [[%Lexeme{positions: [{1, :A}], word: "car"}]]) 37 | 38 | assert [["'car':1B"]] = 39 | query("SELECT $1::tsvector::text", [[%Lexeme{positions: [{1, :B}], word: "car"}]]) 40 | 41 | assert [["'car':1C"]] = 42 | query("SELECT $1::tsvector::text", [[%Lexeme{positions: [{1, :C}], word: "car"}]]) 43 | end 44 | 45 | test "encode tsvector with multiple positions and weights", context do 46 | assert [["'car':1A,2,3B"]] = 47 | query("SELECT $1::tsvector::text", [ 48 | [%Lexeme{positions: [{1, :A}, {2, nil}, {3, :B}], word: "car"}] 49 | ]) 50 | end 51 | 52 | test "decode basic tsvectors", context do 53 | assert [[[%Lexeme{positions: [], word: "1"}]]] = query("SELECT '1'::tsvector", []) 54 | assert [[[%Lexeme{positions: [], word: "1"}]]] = query("SELECT '1 '::tsvector", []) 55 | assert [[[%Lexeme{positions: [], word: "1"}]]] = query("SELECT ' 1'::tsvector", []) 56 | assert [[[%Lexeme{positions: [], word: "1"}]]] = query("SELECT ' 1 '::tsvector", []) 57 | end 58 | 59 | test "decode tsvectors with multiple elements", context do 60 | assert [[[%Lexeme{positions: [], word: "1"}, %Lexeme{positions: [], word: "2"}]]] = 61 | query("SELECT '1 2'::tsvector", []) 62 | 63 | assert [[[%Lexeme{positions: [], word: "1 2"}]]] = query("SELECT '''1 2'''::tsvector", []) 64 | end 65 | 66 | test "decode tsvectors with multiple positions and elements", context do 67 | assert [ 68 | [ 69 | [ 70 | %Lexeme{positions: [{8, nil}], word: "a"}, 71 | %Lexeme{positions: [{1, nil}, {2, :C}, {3, :B}, {4, :A}, {5, nil}], word: "w"} 72 | ] 73 | ] 74 | ] = query("SELECT '''w'':4A,2C,3B,1D,5 a:8'::tsvector", []) 75 | 76 | assert [ 77 | [ 78 | [ 79 | %Lexeme{positions: [{3, :A}], word: "a"}, 80 | %Lexeme{positions: [{2, :A}], word: "b"} 81 | ] 82 | ] 83 | ] = query("SELECT 'a:3A b:2a'::tsvector", []) 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/type_module_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TypeModuleTest do 2 | use ExUnit.Case, async: true 3 | import Postgrex.TestHelper 4 | alias Postgrex, as: P 5 | 6 | @types TestTypes 7 | 8 | setup_all do 9 | on_exit(fn -> 10 | :code.delete(@types) 11 | :code.purge(@types) 12 | end) 13 | 14 | opts = [decode_binary: :reference, null: :custom, json: :fake_json] 15 | Postgrex.TypeModule.define(@types, [], opts) 16 | :ok 17 | end 18 | 19 | setup do 20 | opts = [database: "postgrex_test", backoff_type: :stop, types: @types] 21 | {:ok, pid} = P.start_link(opts) 22 | {:ok, [pid: pid]} 23 | end 24 | 25 | @tag min_pg_version: "9.0" 26 | test "hstore references binaries when decode_binary: :reference", context do 27 | # For OTP 20+ refc binaries up to 64 bytes might be copied during a GC 28 | text = String.duplicate("hello world", 6) 29 | assert [[bin]] = query("SELECT $1::text", [text]) 30 | assert :binary.referenced_byte_size(bin) > byte_size(text) 31 | 32 | assert [[%{"hello" => value}]] = query("SELECT $1::hstore", [%{"hello" => text}]) 33 | assert :binary.referenced_byte_size(value) > byte_size(text) 34 | end 35 | 36 | test "decode null with custom mapping", context do 37 | assert [[:custom]] = query("SELECT NULL", []) 38 | assert [[true, false, :custom]] = query("SELECT true, false, NULL", []) 39 | assert [[true, :custom, false]] = query("SELECT true, NULL, false", []) 40 | assert [[:custom, true, false]] = query("SELECT NULL, true, false", []) 41 | assert [[[:custom, true, false]]] = query("SELECT ARRAY[NULL, true, false]", []) 42 | assert [[{:custom, true, false}]] = query("SELECT ROW(NULL, true, false)", []) 43 | end 44 | 45 | test "encode null with custom mapping", context do 46 | assert [[:custom, :custom]] = query("SELECT $1::text, $2::int", [:custom, :custom]) 47 | 48 | assert [[true, false, :custom]] = 49 | query("SELECT $1::bool, $2::bool, $3::bool", [true, false, :custom]) 50 | 51 | assert [[true, :custom, false]] = 52 | query("SELECT $1::bool, $2::bool, $3::bool", [true, :custom, false]) 53 | 54 | assert [[:custom, true, false]] = 55 | query("SELECT $1::bool, $2::bool, $3::bool", [:custom, true, false]) 56 | 57 | assert [["{NULL,t,f}"]] = query("SELECT ($1::bool[])::text", [[:custom, true, false]]) 58 | end 59 | 60 | test "prepare and execute query with connection mapping", context do 61 | assert (%Postgrex.Query{} = query) = prepare("null", "SELECT $1::text") 62 | assert [[:custom]] = execute(query, [:custom]) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/type_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TypeServerTest do 2 | use ExUnit.Case, async: true 3 | alias Postgrex.TypeSupervisor, as: TM 4 | alias Postgrex.TypeServer, as: TS 5 | 6 | @types Postgrex.DefaultTypes 7 | 8 | test "fetches and unlocks" do 9 | key = make_ref() 10 | server = TM.locate(@types, key) 11 | assert {:lock, ref, {@types, table}} = TS.fetch(server) 12 | assert :ets.info(table, :name) == Postgrex.Types 13 | assert TS.update(server, ref, []) == :ok 14 | end 15 | 16 | test "fetches and exits" do 17 | key = make_ref() 18 | server = TM.locate(@types, key) 19 | task = Task.async(fn -> assert {:lock, _, _} = TS.fetch(server) end) 20 | {:lock, _, types} = Task.await(task) 21 | assert ^server = TM.locate(@types, key) 22 | assert {:lock, _, ^types} = TS.fetch(server) 23 | end 24 | 25 | test "blocks on initial fetch until update returns lock" do 26 | key = make_ref() 27 | server = TM.locate(@types, key) 28 | {:lock, ref, types} = TS.fetch(server) 29 | 30 | task = Task.async(fn -> TS.fetch(server) end) 31 | :timer.sleep(100) 32 | TS.update(server, ref, []) 33 | assert {:lock, _, ^types} = Task.await(task) 34 | end 35 | 36 | test "blocks on later fetch until update returns lock" do 37 | key = make_ref() 38 | server = TM.locate(@types, key) 39 | {:lock, ref, types} = TS.fetch(server) 40 | TS.update(server, ref, []) 41 | 42 | assert {:lock, ref, ^types} = TS.fetch(server) 43 | 44 | task = Task.async(fn -> TS.fetch(server) end) 45 | :timer.sleep(100) 46 | assert Task.yield(task, 0) == nil 47 | TS.update(server, ref, []) 48 | assert {:lock, _, ^types} = Task.await(task) 49 | end 50 | 51 | test "blocks on initial fetch until done returns lock" do 52 | key = make_ref() 53 | server = TM.locate(@types, key) 54 | {:lock, ref, types} = TS.fetch(server) 55 | 56 | task = Task.async(fn -> TS.fetch(server) end) 57 | :timer.sleep(100) 58 | assert Task.yield(task, 0) == nil 59 | TS.done(server, ref) 60 | assert {:lock, _, ^types} = Task.await(task) 61 | end 62 | 63 | test "blocks on later fetch until done returns lock" do 64 | key = make_ref() 65 | server = TM.locate(@types, key) 66 | {:lock, ref, types} = TS.fetch(server) 67 | TS.update(server, ref, []) 68 | 69 | {:lock, ref, ^types} = TS.fetch(server) 70 | 71 | task = Task.async(fn -> TS.fetch(server) end) 72 | :timer.sleep(100) 73 | assert Task.yield(task, 0) == nil 74 | TS.done(server, ref) 75 | assert {:lock, _, ^types} = Task.await(task) 76 | end 77 | 78 | test "fetches existing table even if parent crashes" do 79 | key = make_ref() 80 | server = TM.locate(@types, key) 81 | 82 | task = 83 | Task.async(fn -> 84 | {:lock, ref, types} = TS.fetch(server) 85 | TS.update(server, ref, []) 86 | types 87 | end) 88 | 89 | types = Task.await(task) 90 | wait_until_dead(task.pid) 91 | 92 | task = Task.async(fn -> TS.fetch(server) end) 93 | assert {:lock, _, ^types} = Task.await(task) 94 | end 95 | 96 | test "the lock is granted to single process one by one" do 97 | key = make_ref() 98 | server = TM.locate(@types, key) 99 | 100 | {:lock, ref, types} = TS.fetch(server) 101 | TS.update(server, ref, []) 102 | 103 | parent = self() 104 | 105 | task = fn -> 106 | result = TS.fetch(server) 107 | send(parent, {self(), result}) 108 | 109 | case result do 110 | {:lock, ref2, _} -> 111 | assert_receive {^parent, :go} 112 | TS.update(server, ref2, []) 113 | 114 | _ -> 115 | :ok 116 | end 117 | end 118 | 119 | {:ok, _} = Task.start_link(task) 120 | {:ok, _} = Task.start_link(task) 121 | {:ok, _} = Task.start_link(task) 122 | 123 | for _ <- 1..3 do 124 | assert_receive {pid, {:lock, _, ^types}} 125 | :timer.sleep(100) 126 | :sys.get_state(server) 127 | refute_received _ 128 | send(pid, {parent, :go}) 129 | end 130 | 131 | assert {:lock, _, ^types} = TS.fetch(server) 132 | end 133 | 134 | test "does not fetch existing table if parent crashes and timeout passes" do 135 | Application.put_env(:postgrex, :type_server_reap_after, 0) 136 | key = make_ref() 137 | 138 | task = 139 | Task.async(fn -> 140 | server = TM.locate(@types, key) 141 | {:lock, ref, types} = TS.fetch(server) 142 | TS.update(server, ref, []) 143 | {server, types} 144 | end) 145 | 146 | {server1, types1} = Task.await(task) 147 | mref = Process.monitor(server1) 148 | assert_receive {:DOWN, ^mref, _, _, _} 149 | 150 | server2 = TM.locate(@types, key) 151 | refute server1 == server2 152 | 153 | task = Task.async(fn -> TS.fetch(server2) end) 154 | assert {:lock, _, types2} = Task.await(task) 155 | 156 | assert types1 != types2 157 | assert {_, table1} = types1 158 | assert :ets.info(table1, :name) == :undefined 159 | after 160 | Application.put_env(:postgrex, :type_server_reap_after, 3 * 60_000) 161 | end 162 | 163 | test "gives lock to another process if original holder crashes before fetch" do 164 | key = make_ref() 165 | server = TM.locate(@types, key) 166 | 167 | task = Task.async(fn -> TS.fetch(server) end) 168 | assert {:lock, _ref, types} = Task.await(task) 169 | wait_until_dead(task.pid) 170 | 171 | assert {:lock, _ref, ^types} = Task.async(fn -> TS.fetch(server) end) |> Task.await() 172 | end 173 | 174 | test "error waiting process if original holder crashes after fetch" do 175 | key = make_ref() 176 | server = TM.locate(@types, key) 177 | top = self() 178 | 179 | {:ok, pid} = 180 | Task.start(fn -> 181 | send(top, TS.fetch(server)) 182 | :timer.sleep(:infinity) 183 | end) 184 | 185 | assert_receive {:lock, _, types} 186 | task = Task.async(fn -> TS.fetch(server) end) 187 | 188 | Process.exit(pid, :kill) 189 | wait_until_dead(pid) 190 | assert {:lock, _, ^types} = Task.await(task) 191 | end 192 | 193 | defp wait_until_dead(pid) do 194 | ref = Process.monitor(pid) 195 | receive do: ({:DOWN, ^ref, _, _, _} -> :ok) 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /test/utils/envs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Utils.EnvsTest do 2 | use ExUnit.Case, async: false 3 | 4 | ExUnit.Case.register_attribute(__MODULE__, :env) 5 | 6 | setup ctx do 7 | envs = 8 | for {key, value} <- ctx.registered.env do 9 | key = key |> to_string() |> String.upcase() 10 | old = System.get_env(key) 11 | 12 | if value do 13 | System.put_env(key, value) 14 | else 15 | System.delete_env(key) 16 | end 17 | 18 | {key, old} 19 | end 20 | 21 | on_exit(fn -> 22 | for {key, value} <- envs do 23 | if value do 24 | System.put_env(key, value) 25 | else 26 | System.delete_env(key) 27 | end 28 | end 29 | end) 30 | 31 | {:ok, opts: Postgrex.Utils.default_opts([])} 32 | end 33 | 34 | describe "PGHOST" do 35 | @env PGHOST: nil 36 | test "by default it is set to `localhost`", ctx do 37 | assert ctx.opts[:hostname] == "localhost" 38 | end 39 | 40 | @env PGHOST: "foobar" 41 | test "if the host is 'regular' hostname, then it sets hostname", ctx do 42 | assert ctx.opts[:hostname] == "foobar" 43 | end 44 | 45 | @env PGHOST: "127.0.0.1" 46 | test "if the host is IPv4 address then it sets hostname", ctx do 47 | assert ctx.opts[:hostname] == "127.0.0.1" 48 | end 49 | 50 | @env PGHOST: "[::1]" 51 | test "if the host is IPv6 address then it sets hostname", ctx do 52 | assert ctx.opts[:hostname] == "[::1]" 53 | end 54 | 55 | @env PGHOST: "/tmp/example" 56 | test "if the host is path-like (UNIX) then it sets socket_dir", ctx do 57 | assert ctx.opts[:socket_dir] == "/tmp/example" 58 | end 59 | 60 | @env PGHOST: ~S"C:\\example" 61 | test "if the host is path-like (Windows) then it sets socket_dir", ctx do 62 | assert ctx.opts[:socket_dir] == ~S"C:\\example" 63 | end 64 | 65 | @env PGHOST: ~S"C://example" 66 | test "if the host is path-like (Windows alt) then it sets socket_dir", ctx do 67 | assert ctx.opts[:socket_dir] == ~S"C://example" 68 | end 69 | 70 | @env PGHOST: "@foo" 71 | test "if the host is abstract socket address it sets socket", ctx do 72 | assert ctx.opts[:socket] == <<0, "foo">> 73 | end 74 | end 75 | 76 | describe "PGHOST with manual overrides" do 77 | @env PGHOST: "/test/socket" 78 | test "respects explicit hostname even if PGHOST is set" do 79 | opts = Postgrex.Utils.default_opts(hostname: "localhost") 80 | 81 | assert Keyword.get(opts, :hostname) == "localhost" 82 | refute Keyword.has_key?(opts, :socket_dir) 83 | end 84 | 85 | @env PGHOST: "/test/socket" 86 | test "respects explicit endpoints even if PGHOST is set" do 87 | opts = Postgrex.Utils.default_opts(endpoints: [{"localhost", 5432}]) 88 | 89 | assert Keyword.get(opts, :endpoints) == [{"localhost", 5432}] 90 | refute Keyword.has_key?(opts, :socket_dir) 91 | end 92 | 93 | @env PGHOST: "/test/socket" 94 | test "respects explicit socket even if PGHOST is set" do 95 | opts = Postgrex.Utils.default_opts(socket: "/var/run/postgresql") 96 | 97 | assert Keyword.get(opts, :socket) == "/var/run/postgresql" 98 | refute Keyword.has_key?(opts, :socket_dir) 99 | end 100 | 101 | @env PGHOST: "/test/socket" 102 | test "respects explicit socket_dir even if PGHOST is set" do 103 | opts = Postgrex.Utils.default_opts(socket_dir: "/another/test/socket") 104 | 105 | assert Keyword.get(opts, :socket_dir) == "/another/test/socket" 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UtilsTest do 2 | use ExUnit.Case, async: true 3 | 4 | describe "parse_version/1" do 5 | test "parses simple 3-part version strings" do 6 | segments = Postgrex.Utils.parse_version("10.1.0") 7 | assert segments == {10, 1, 0} 8 | end 9 | 10 | test "parses complex version strings" do 11 | segments = Postgrex.Utils.parse_version("10.2 (Debian 10.2-1.pgdg90+1)") 12 | assert segments == {10, 2, 0} 13 | end 14 | 15 | test "parses 3 part version strings" do 16 | segments = Postgrex.Utils.parse_version("10.2.1 (Debian 10.2-1.pgdg90+1)") 17 | assert segments == {10, 2, 1} 18 | end 19 | 20 | test "parses 4 part version strings" do 21 | segments = Postgrex.Utils.parse_version("9.5.5.10 ___") 22 | assert segments == {9, 5, 5} 23 | end 24 | 25 | test "parses beta version strings" do 26 | segments = Postgrex.Utils.parse_version("12beta1 (Debian 10.2-1.pgdg90+1)") 27 | 28 | assert segments == {12, 0, 0} 29 | 30 | assert Postgrex.Utils.parse_version("12alpha1 (Debian 10.2-1.pgdg90+1)") == 31 | Postgrex.Utils.parse_version("12beta1 (Debian 10.2-1.pgdg90+1)") 32 | end 33 | end 34 | end 35 | --------------------------------------------------------------------------------