Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
62ff22f
1.0 release readiness: protocol fixes, org-transfer cleanup, API polish
quinnj Aug 6, 2026
ac85a2f
Address independent review findings
quinnj Aug 6, 2026
9b02386
Harden copy_in/copy_out against user-IO failures mid-copy
quinnj Aug 6, 2026
8bc5a42
Address security review findings
quinnj Aug 6, 2026
3e362b9
Address security verification findings
quinnj Aug 6, 2026
18cc378
Fix cancel TLS upgrade being skipped, and desync on bad message length
quinnj Aug 6, 2026
136d060
Fix async-message desync in waitfor, and notification read deadline
quinnj Aug 6, 2026
302aa97
Fix leaked read deadline, and notification waiting over TLS
quinnj Aug 6, 2026
6323566
Fix silent array data loss, connection-string handling, and fd leak
quinnj Aug 6, 2026
a9c348a
Document the driver-style behavior interface
quinnj Aug 6, 2026
fa9bad1
Fix array truncation on ']', schema narrowing, and cursor transaction…
quinnj Aug 6, 2026
2cf22bf
Fix transaction state surviving a failed commit, and cursor double-close
quinnj Aug 6, 2026
e8451c0
Use the non-localized severity from ErrorResponse
quinnj Aug 6, 2026
007ceaa
Fix lost server errors on commit failure, and pooled transaction leakage
quinnj Aug 6, 2026
0da51f2
Fix timestamp ranges, "char" zero values, and pin DateStyle
quinnj Aug 6, 2026
c1ae7ba
Align session date formats without new startup parameters
quinnj Aug 6, 2026
eab05bd
Fix interval-style gap, raw-SQL transactions in the pool, empty sslmode
quinnj Aug 6, 2026
826babf
Fix range registration, BC/wide dates, char escapes, DateStyle order
quinnj Aug 6, 2026
6f65316
Fix raw-SQL transaction safety, multibyte range bounds, offset seconds
quinnj Aug 6, 2026
4fea291
fix: close 1.0 release blockers
quinnj Aug 6, 2026
119ab47
test: cover connection-level wire logging
quinnj Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,60 @@ jobs:
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1

lower-bounds:
name: Lower dependency bounds - Julia 1.10 - ubuntu-latest
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
- uses: julia-actions/setup-julia@v3
with:
version: '1.10'
arch: x64
- uses: julia-actions/cache@v3
- name: Test declared dependency floors
run: |
using Pkg
mktempdir() do env
Pkg.activate(env)
Pkg.develop(path=ENV["GITHUB_WORKSPACE"])
Pkg.add([
Pkg.PackageSpec(name="ConcurrentUtilities", version="2.1.0"),
Pkg.PackageSpec(name="DBInterface", version="2.5.0"),
Pkg.PackageSpec(name="JSON", version="1.0.0"),
Pkg.PackageSpec(name="MD5", version="0.2.0"),
Pkg.PackageSpec(name="Parsers", version="2.5.4"),
Pkg.PackageSpec(name="Reseau", version="1.1.1"),
Pkg.PackageSpec(name="SASLAuth", version="1.0.0"),
Pkg.PackageSpec(name="StructUtils", version="2.3.0"),
Pkg.PackageSpec(name="Tables", version="1.0.0"),
Pkg.PackageSpec(name="URIs", version="1.0.0"),
])
Pkg.test("Postgres")
end
shell: julia --color=yes {0}

postgres-versions:
name: PostgreSQL ${{ matrix.postgres }} - Julia 1 - ubuntu-latest
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
# PostgreSQL 16 is covered by the main OS and Julia-version matrix.
postgres: ['14', '15', '17', '18']
env:
POSTGRES_IMAGE: postgres:${{ matrix.postgres }}
steps:
- uses: actions/checkout@v6
- uses: julia-actions/setup-julia@v3
with:
version: '1'
arch: x64
- uses: julia-actions/cache@v3
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1

docs:
name: Documentation
runs-on: ubuntu-latest
Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Jacob Quinn and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
22 changes: 0 additions & 22 deletions LICENSE.md

This file was deleted.

19 changes: 12 additions & 7 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ ConcurrentUtilities = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
DBInterface = "a10d1c49-ce27-4219-8d33-6db1a4562965"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
MD5 = "6ac74813-4b46-53a4-afec-0b5dc9d7885c"
Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Expand All @@ -19,24 +18,30 @@ URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[compat]
ConcurrentUtilities = "2"
Aqua = "0.8"
ConcurrentUtilities = "2.1"
DBInterface = "2.5"
Dates = "1.10"
Harbor = "1"
JSON = "1"
MD5 = "0.2"
Parsers = "0.3, 1, 2"
Reseau = "1.1"
Parsers = "2.5.4"
Random = "1.10"
Reseau = "1.1.1"
SASLAuth = "1"
StructUtils = "2"
Sockets = "1.10"
StructUtils = "2.3"
Tables = "1"
Test = "1.10"
URIs = "1"
UUIDs = "1.10"
julia = "1.10"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Harbor = "af79dbb9-1a80-47ad-8928-192a4af69376"
JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Harbor", "JuliaC", "Sockets", "Test"]
test = ["Aqua", "Harbor", "Sockets", "Test"]
46 changes: 31 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Postgres.jl

Postgres.jl is a PostgreSQL client that implements the v3 wire protocol with `DBInterface` and `Tables` integration.
[![CI](https://github.com/JuliaDatabases/Postgres.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/JuliaDatabases/Postgres.jl/actions/workflows/CI.yml)
[![docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaDatabases.github.io/Postgres.jl/dev/)
[![codecov](https://codecov.io/gh/JuliaDatabases/Postgres.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaDatabases/Postgres.jl)

Postgres.jl is a PostgreSQL client written in Julia that implements the v3 wire protocol with `DBInterface` and `Tables` integration.

## Installation

Expand All @@ -12,10 +16,10 @@ Pkg.add("Postgres")
## Quick start

```julia
using Postgres, DBInterface, Tables
using Postgres
DBInterface.connect(Postgres.Connection, "host=127.0.0.1;port=5432;user=postgres;password=postgres;dbname=postgres") do conn
rows = Tables.rowtable(DBInterface.execute(conn, "SELECT 1 AS a"))
@show rows[1].a
row = only(DBInterface.execute(conn, "SELECT 1 AS a"))
@show row.a
end
```

Expand All @@ -32,11 +36,15 @@ Connection options support:
- libpq-style keyword strings such as `host=127.0.0.1 port=5432 user=postgres dbname=postgres`.
- PostgreSQL URIs such as `postgresql://postgres:postgres@127.0.0.1:5432/postgres`.
- Environment defaults: `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGAPPNAME`, `PGCONNECT_TIMEOUT`, and TLS-related `PGSSL*` variables.
- `sslmode` values: `disable`, `prefer`, `require`, `verify-full` (only `verify-full` enforces certificate verification).
- TLS files: `sslrootcert`, `sslcert`, `sslkey`, `sslcapath`.
- `sslmode` values: `disable`, `prefer` (the default), `require`, `verify-full`. Only `verify-full` verifies the server's certificate; `require` encrypts without authenticating the server, and the default `prefer` falls back to an unencrypted connection if the server declines TLS. Use `verify-full` with `sslrootcert` when the connection needs to be authenticated.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Describe sslcapath according to the transport behavior

This new limitation is not true for the supported Reseau releases. Its ca_file input explicitly accepts a CA bundle or hashed CA directory, and its trust-store loader branches on isdir. Passing sslcapath through that field therefore does support a directory. Also, this code chooses sslrootcert when both options are set; sslcapath is not loaded as an additional file.

Please test a CA directory and document the real precedence and supported path forms. If the intent is to forbid directories, validate and reject them instead of documenting behavior that the code does not enforce.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retested the documentation change at 007ceaa. The new fallback precedence is accurate: Postgres.jl selects sslrootcert when both are set, otherwise it passes sslcapath as Reseau ca_file.

The directory limitation is still false for resolved Reseau 1.3.4. Its public Config docs define ca_file as a CA bundle or hashed CA directory. _tls_load_trust_certificates branches on isdir, scans regular files for PEM certificate blocks, and builds the trust store. I passed a real directory containing this review CA certificate to _tls_load_trust_store; it loaded one root successfully. Please document sslcapath as a fallback CA path that may be a bundle or directory, and add an end-to-end verify-full directory case.

- TLS files: `sslrootcert`, `sslcert`, `sslkey`, and `sslcapath` (`sslcapath` is a fallback CA bundle or directory, used only when `sslrootcert` is unset and ignored otherwise). `sslservername` overrides the TLS server name when connecting to a pre-resolved address; under `verify-full` it is also the name the certificate is verified against, so it must name the server you intend to authenticate.
- `connect_timeout` (seconds) and `statement_timeout` (milliseconds).
- `application_name` and `statement_cache_maxsize`.

See the [1.0 support policy](https://JuliaDatabases.github.io/Postgres.jl/dev/support/)
for tested Julia and PostgreSQL versions, TLS limits, and transaction-pooler
requirements.

You can also use `ConnectionParams`:

```julia
Expand All @@ -51,9 +59,9 @@ DBInterface.close!(conn)
```julia
using Postgres, DBInterface, Tables
conn = DBInterface.connect(Postgres.Connection, "host=127.0.0.1;user=postgres;password=postgres;dbname=postgres")
rows = Tables.rowtable(DBInterface.execute(conn, "SELECT $1::int AS val", (42,)))
rows = Tables.rowtable(DBInterface.execute(conn, raw"SELECT $1::int AS val", (42,)))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Make the documented first run work after the stated install step

Pkg.add("Postgres") followed by the quick-start import fails in a clean environment with ArgumentError: Package DBInterface not found in current path. DBInterface, Tables, and StructUtils are transitive dependencies, so users cannot import them directly unless they also add them to their environment. The manual has the same problem with using Postgres, Tables, and it closes conn before later snippets reuse it.

Please make the default quick start runnable after only Pkg.add("Postgres"). Use the DBInterface binding that Postgres intentionally exports and avoid Tables in the minimal first example. For examples that require Tables or StructUtils, state that users must add those optional integration packages. Please keep the package export surface narrow; exporting more dependencies is not the fix.

@show rows[1].val
stmt = DBInterface.prepare(conn, "SELECT $1::int AS val")
stmt = DBInterface.prepare(conn, raw"SELECT $1::int AS val")
rows = Tables.rowtable(DBInterface.execute(stmt, (7,)))
DBInterface.close!(stmt)
DBInterface.close!(conn)
Expand Down Expand Up @@ -87,7 +95,7 @@ StructUtils.@tags struct ProfileSummary
createdAt::DateTime &(postgres=(name=:created_at,),)
end

profile = DBInterface.execute(conn, """
profile = DBInterface.execute(conn, raw"""
SELECT profile_id, first_name, last_name, created_at
FROM profiles
WHERE profile_id = $1
Expand All @@ -103,7 +111,8 @@ profiles = DBInterface.execute(conn, """

`Postgres.command_tag(result)` and `Postgres.rows_affected(result)` expose PostgreSQL command completion metadata.

Statement caching is LRU-based. Set `statement_cache_maxsize=0` to disable caching.
Explicit named prepared statements use an LRU backend cache. Caller handles are
independent. Set `statement_cache_maxsize=0` to disable this cache.

```julia
using Postgres, DBInterface
Expand Down Expand Up @@ -183,15 +192,22 @@ DBInterface.close!(conn)
```

`Numeric` values are returned as `Postgres.Numeric`, `interval` values as `Dates.Period` or `Dates.CompoundPeriod`, and range types as `Postgres.PostgresRange{T}`.
Custom enum, composite, and range registration controls result decoding. Those
custom Julia values are not accepted as direct query parameters in 1.0; bind a
PostgreSQL text representation with an explicit SQL cast instead.

## Query logging and driver styles

## Query logging
Driver behavior — query logging, server notices, asynchronous notifications — is customized by defining a driver "style": subtype `Postgres.AbstractPostgresStyle`, overload the behavior hooks for it, and pass an instance via the `style` connection keyword.

```julia
using Postgres, DBInterface
conn = DBInterface.connect(Postgres.Connection, "host=127.0.0.1;user=postgres;password=postgres;dbname=postgres")
Postgres.set_query_logger!(conn) do event, info
@show event info.success info.duration_ns
end

struct LoggingStyle <: Postgres.AbstractPostgresStyle end
Postgres.query_logging_enabled(::LoggingStyle) = true
Postgres.query_logger(::LoggingStyle, event::Symbol, info::NamedTuple) = @info "query" event info.success info.duration_ns

conn = DBInterface.connect(Postgres.Connection, "host=127.0.0.1;user=postgres;password=postgres;dbname=postgres"; style=LoggingStyle())
DBInterface.execute(conn, "SELECT 1")
DBInterface.close!(conn)
```
Expand Down
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ makedocs(
pages = [
"Home" => "index.md",
"Manual" => "manual.md",
"Support Policy" => "support.md",
],
)

if get(ENV, "POSTGRES_DOCS_DEPLOY", "false") == "true"
deploydocs(repo = "github.com/quinnj/Postgres.jl.git", push_preview = true)
deploydocs(repo = "github.com/JuliaDatabases/Postgres.jl.git", push_preview = true)
end
39 changes: 29 additions & 10 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Postgres.jl is a PostgreSQL client that speaks the v3 wire protocol with `DBInterface` and `Tables` integration.

See the [Manual](@ref) for a guided walk through connections, queries, prepared statements, transactions, cancellation, notifications, and type translation.
See the [1.0 Support Policy](@ref) for tested versions and explicit limits.

## Installation

Expand All @@ -17,11 +18,14 @@ Postgres.jl accepts DSN strings or PostgreSQL URIs and supports:

- libpq-style keyword strings such as `host=127.0.0.1 port=5432 user=postgres dbname=postgres`.
- Environment defaults from `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGAPPNAME`, `PGCONNECT_TIMEOUT`, and TLS-related `PGSSL*` variables.
- `sslmode` values: `disable`, `prefer`, `require`, `verify-full` (only `verify-full` verifies certificates).
- TLS files: `sslrootcert`, `sslcert`, `sslkey`, `sslcapath`.
- `sslmode` values: `disable`, `prefer` (the default), `require`, `verify-full`. Only `verify-full` verifies the server's certificate; `require` encrypts without authenticating the server, and the default `prefer` falls back to an unencrypted connection if the server declines TLS. Use `verify-full` with `sslrootcert` when the connection needs to be authenticated.
- TLS files: `sslrootcert`, `sslcert`, `sslkey`, and `sslcapath` (`sslcapath` is a fallback CA bundle or directory, used only when `sslrootcert` is unset and ignored otherwise). `sslservername` overrides the TLS server name when connecting to a pre-resolved address; under `verify-full` it is also the name the certificate is verified against, so it must name the server you intend to authenticate.
- `connect_timeout` (seconds), `statement_timeout` (milliseconds).
- `application_name` and `statement_cache_maxsize`.

Options that request unsupported security or server-selection behavior are
rejected. They are not silently ignored.

```julia
using Postgres, DBInterface
conn = DBInterface.connect(Postgres.Connection, "postgresql://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable")
Expand All @@ -33,7 +37,7 @@ DBInterface.close!(conn)
```julia
using Postgres, DBInterface, Tables
conn = DBInterface.connect(Postgres.Connection, "host=127.0.0.1;user=postgres;password=postgres;dbname=postgres")
rows = Tables.rowtable(DBInterface.execute(conn, "SELECT $1::int AS val", (42,)))
rows = Tables.rowtable(DBInterface.execute(conn, raw"SELECT $1::int AS val", (42,)))
@show rows[1].val
DBInterface.close!(conn)
```
Expand Down Expand Up @@ -65,7 +69,7 @@ StructUtils.@tags struct ProfileSummary
createdAt::DateTime &(postgres=(name=:created_at,),)
end

profile = DBInterface.execute(conn, """
profile = DBInterface.execute(conn, raw"""
SELECT profile_id, first_name, last_name, created_at
FROM profiles
WHERE profile_id = $1
Expand All @@ -79,12 +83,12 @@ profiles = DBInterface.execute(conn, """
""", (), Vector{ProfileSummary})
```

Prepared statements are cached with LRU eviction; disable caching via `statement_cache_maxsize=0`.
Explicit named prepared statements use an LRU backend cache; disable it via `statement_cache_maxsize=0`.

```julia
using Postgres, DBInterface, Tables
conn = DBInterface.connect(Postgres.Connection, "host=127.0.0.1;user=postgres;password=postgres;dbname=postgres"; statement_cache_maxsize=5)
stmt = DBInterface.prepare(conn, "SELECT $1::int AS val")
stmt = DBInterface.prepare(conn, raw"SELECT $1::int AS val")
rows = Tables.rowtable(DBInterface.execute(stmt, (7,)))
DBInterface.close!(stmt)
DBInterface.close!(conn)
Expand Down Expand Up @@ -162,12 +166,16 @@ DBInterface.close!(conn)

## Query logging

Query logging (and other driver behavior) is customized with a driver style; see the [Manual](@ref) for details.

```julia
using Postgres, DBInterface
conn = DBInterface.connect(Postgres.Connection, "host=127.0.0.1;user=postgres;password=postgres;dbname=postgres")
Postgres.set_query_logger!(conn) do event, info
@show event info.success info.duration_ns
end

struct LoggingStyle <: Postgres.AbstractPostgresStyle end
Postgres.query_logging_enabled(::LoggingStyle) = true
Postgres.query_logger(::LoggingStyle, event::Symbol, info::NamedTuple) = @info "query" event info.success info.duration_ns

conn = DBInterface.connect(Postgres.Connection, "host=127.0.0.1;user=postgres;password=postgres;dbname=postgres"; style=LoggingStyle())
DBInterface.execute(conn, "SELECT 1")
DBInterface.close!(conn)
```
Expand All @@ -187,6 +195,17 @@ DBInterface.close!(pool)

`Postgres.Error` includes SQLSTATE information. Use `Postgres.cancel_query!(conn)` to cancel a running query.

## Reference

```@autodocs
Modules = [Postgres]
```

```@docs
Postgres.Error
Postgres.Notification
Postgres.Numeric
Postgres.PostgresRange
Postgres.ConnectionParams
Postgres.parse_dsn
```
Loading