diff --git a/go.mod b/go.mod index 2e1c128..bfefec0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.0 toolchain go1.26.5 require ( - github.com/jackc/pgx/v5 v5.10.0 + github.com/jackc/pgx/v5 v5.11.0 github.com/spice-framework/spice v0.0.0-20260805222830-a2ecd56df246 ) diff --git a/go.sum b/go.sum index 3beb183..e6e2bd7 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= -github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/vendor/github.com/jackc/pgx/v5/.gitignore b/vendor/github.com/jackc/pgx/v5/.gitignore index a2ebbe9..ff05307 100644 --- a/vendor/github.com/jackc/pgx/v5/.gitignore +++ b/vendor/github.com/jackc/pgx/v5/.gitignore @@ -22,6 +22,26 @@ _testmain.go *.exe .envrc -/.testdb + +# Per-checkout development state: this checkout's allocated TCP ports, the values derived from +# them, its decoded client certificates, and its own PostgreSQL/CockroachDB clusters. A git +# worktree is the native equivalent of a second devcontainer instance, so this is instance-local +# by definition -- never shared, never committed. See DEVELOPMENT.md. +/.dev/ + +# port-tamer writes this checkout's allocation to .dev/ports.env (the --state-file every task +# passes). These are where a bare `port-tamer` run at the repo root would put a stray second +# allocation instead; ignored so it cannot be committed by accident. +/.port-tamer.env +/.port-tamer.env.lock + +# Personal, machine-local mise overrides (mise.toml IS committed/shared). Git has no knowledge of +# mise's conventions, so this must be ignored explicitly. +/mise.local.toml + +# Provisioned on demand with `rake references:setup`; never committed. +/references/ .DS_Store + +/.vscode diff --git a/vendor/github.com/jackc/pgx/v5/Brewfile b/vendor/github.com/jackc/pgx/v5/Brewfile new file mode 100644 index 0000000..e35acd7 --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/Brewfile @@ -0,0 +1,15 @@ +# Host dependencies only. Project tools and language runtimes belong in +# mise.toml; scripts/setup-host installs mise itself when necessary. + +# Server and client binaries for the checkout-local test matrix. +brew "postgresql@14" +brew "postgresql@15" +brew "postgresql@16" +brew "postgresql@17" +brew "postgresql@18" + +# mise compiles Ruby from source and links against these. +brew "openssl@3" +brew "readline" +brew "libyaml" +brew "gmp" diff --git a/vendor/github.com/jackc/pgx/v5/CHANGELOG.md b/vendor/github.com/jackc/pgx/v5/CHANGELOG.md index 12e633a..98d8ff7 100644 --- a/vendor/github.com/jackc/pgx/v5/CHANGELOG.md +++ b/vendor/github.com/jackc/pgx/v5/CHANGELOG.md @@ -1,3 +1,190 @@ +# 5.11.0 (September 7, 2026) + +This release adds direct PostgreSQL type scanning through `database/sql` on Go 1.27, improves compatibility with +libpq connection strings and PostgreSQL date/time values, and includes further decoder hardening. See Changes for +connection-string and date/time behavior changes that may affect existing applications. + +## Features + +* stdlib: support Go 1.27's `driver.RowsColumnScanner`, allowing PostgreSQL types such as arrays and ranges to be + scanned directly into Go values without `pgtype.Map.SQLScanner`. Existing `database/sql` scalar conversions and + `sql.Scanner` behavior are preserved. The minimum supported Go version remains 1.25. +* Add `Rows.TypeMap` to expose the type map used to decode rows, including rows created by `RowsFromResultReader` + that have no underlying `Conn`. Custom implementations of `Rows`, including mocks, must add this method. +* pgconn: add `Config.MaxProtocolMessageBodyLen` to configure the maximum incoming protocol message body size + (carter-ya) +* pgconn: add `ErrReadOnlyConnection`, `ErrReadWriteConnection`, `ErrPrimaryConnection`, and `ErrStandbyConnection` + sentinel errors for `target_session_attrs` validation, allowing callers to use `errors.Is` (Adrian-Stefan Mares) +* pgxpool: accept `pool_ping_timeout` in connection strings to configure `Config.PingTimeout`. The default is zero; + zero and negative durations mean no timeout (1991santhu) + +## Changes + +* Name-based row-to-struct mapping now matches explicit `db` tags case-insensitively, with exact matches taking + precedence so tags can still distinguish quoted column names that differ only by case (AlisinaDevelo) +* pgconn: resolve the OS user account only when no user is supplied by the connection string, environment, or service + file, avoiding unnecessary account lookups and crashes in some restricted container environments. Home-directory + defaults for password, service, and TLS files remain available independently of the account lookup. On Unix these + now use `$HOME` rather than the OS account's home directory (Mohamed MAACHE) + +* pgtype: `date`, `timestamp` and `timestamptz` text values are now parsed and written by a hand-written parser and + encoder for PostgreSQL's ISO date/time format instead of `time.Parse` and `time.Format`. Go's layout language cannot + express a variable-width year or the BC era, which is the root of the bugs below. The text scan path is roughly 2.5x + faster for `timestamp` and `timestamptz`. Bug fixes: + * `timestamp` and `timestamptz` no longer silently move February 29 of a BC leap year to March 1 when encoding. + `time.Date(-4712, 2, 29, ...)` was written as `4713-03-01 BC` and is now written as `4713-02-29 BC`. This + affected ordinary four-digit BC years, not only extended-range ones. `date` was never affected. + * `timestamp` and `timestamptz` can now scan BC leap days. `4713-02-29 BC` previously failed with + `day out of range`. `date` could already scan them. + * Years past 9999 can now be scanned. `10000-01-02 03:04:05` previously failed to parse, so `timestamp` and + `timestamptz` values at the high end of PostgreSQL's range were unreadable over the simple protocol and in any + other text-format result. + * `time.Time` arguments in the simple protocol now encode BC dates correctly, using the same timestamp encoder. + * Fractional seconds beyond microsecond precision are rounded the way the server rounds them (round half to even, + carrying into the rest of the value) instead of being kept at full precision. PostgreSQL never sends more than six + fractional digits, so this only affects values from other sources. + + Behavior changes: + * `date` now rejects impossible dates instead of normalizing them. `2024-02-30` returned `2024-03-01` and + `2024-13-01` returned `2025-01-01`; both are now errors. `timestamp` and `timestamptz` already rejected them. + * All three types now reject values outside PostgreSQL's range for that type, in the binary format as well as the + text format. PostgreSQL never sends out-of-range dates, so this only affects corrupt or hand-built input; the range + is checked in both formats so that whether a value is accepted does not depend on `QueryExecMode`. + `timestamptz` also rejects time zone displacements outside PostgreSQL's signed 32-bit seconds range, while accepting + the wider offsets emitted for POSIX time zones, such as `+16`. + * `timestamptz` values scanned from the text format are now returned in `time.Local`, or in `ScanLocation` when it is + set, matching what the binary format has always returned. Previously the text path kept whatever location + `time.Parse` derived from the offset the server sent, so the same value scanned in the two formats could report a + different `Location()` and `Zone()`. The instant is unchanged, but everything that renders the location changes + with it: `Timestamptz.MarshalJSON` now writes the client's offset rather than the server's, so a value the server + sent as `+05:30` marshals as `2024-01-01T13:34:05-08:00` on a UTC-8 client instead of `2024-01-02T03:04:05+05:30`, + and `DecodeDatabaseSQLValue` hands `database/sql` a `time.Time` in that same location. Set the codec's + `ScanLocation` to `time.UTC` to pin the location regardless of the client's zone. + * Error messages from these paths have changed. + +* pgconn: connection URIs (`postgres://...`) are now parsed by a new parser designed to exactly match libpq's URI + parser behavior instead of `net/url`, + making pgx accept and reject exactly the same URIs as libpq (verified by differential fuzzing against libpq itself). + Most connection strings are unaffected. Edge-case behavior changes, all matching libpq: + * `+` in query values is literal, no longer decoded as a space. + * Malformed percent-encoding is a parse error instead of the parameter being silently dropped. `%00` is rejected. + * Leading/trailing spaces in URI components are trimmed; interior spaces are a parse error (encode them as `%20`). + * `#` is ordinary data, not a fragment delimiter. + * The userinfo terminator is the first `@` before any `/` (previously the last `@`). + * When a query parameter is repeated, the last occurrence wins (previously the first). + * `ssl=true` is accepted as an alias for `sslmode=require` in URIs (JDBC compatibility). A repeated `ssl` key + follows the same last-occurrence-wins rule as other repeated parameters, even across the rewrite to `sslmode`. If + the final `ssl` value is not `true`, an independent explicit `sslmode` remains in effect. + * Multiple hosts with mixed port specs are positionally aligned: `postgres://h1,h2:5433/db` now means h1:5432 and + h2:5433 (previously both hosts got port 5433). A port list that is neither a single port nor exactly one port per + host is an error (`could not match N port numbers to M hosts`), also for keyword/value connection strings. + * An IPv6 address in a URI must be enclosed in brackets. A bare `postgres://::1/db` was previously accepted as host + `::1`; it is now read as an empty host followed by port `:1` and fails with an invalid port error. Write it as + `postgres://[::1]/db`. + * Empty host list elements (e.g. `h1,,h2`) get the default host instead of being dropped. Likewise, an empty host in + a keyword/value string (`host=`) now means the default host -- typically the Unix socket directory -- where it + previously meant a TCP connection to an empty hostname. + * An empty port (`?port=` in a URI or `port=` in a keyword/value string) now means the default port 5432 for the + affected hosts; previously it was an invalid port error. Like any connection-string port, a present-but-empty port + takes precedence over `PGPORT`. + * ASCII control characters (tab, newline, ...) in a URI are ordinary data bytes, as they are to libpq; `net/url` + rejected any URI containing one. The exception is a literal NUL byte, which is still rejected, as `net/url` did. + (libpq never sees one -- C strings end at the first NUL -- but in Go a raw NUL could otherwise pass through into + the NUL-delimited startup message and inject extra parameters.) + + Unlike libpq, unrecognized URI query parameters are still accepted (they become runtime parameters or pgx-specific + options). Parse error messages avoid quoting the unredacted connection string and redact recognizable password + fields on a best-effort basis. Invalid connection strings can be structurally ambiguous, so password redaction + cannot be guaranteed for every malformed input. + +* pgconn: keyword/value connection strings (`host=... user=...`) now match libpq's parser exactly, the same treatment + the URI parser received above and verified the same way, by differential fuzzing against libpq itself. Most + connection strings are unaffected. Behavior changes, all matching libpq: + * A backslash escapes whatever character follows it and is dropped, where previously only `\\` and `\'` were + unescaped and every other backslash was kept. A value containing a backslash must now escape it, as libpq + requires: `sslcert=C:\path\to\cert` reads as `C:pathtocert` and has to be written `sslcert=C:\\path\\to\\cert`. + This mainly affects Windows certificate and key paths, which previously came through intact without doubling. + * A trailing backslash in an unquoted value escapes the end of the string, so it is dropped and the value ends + there; it was previously rejected with `invalid backslash`. Inside a quoted value the escaped terminator leaves + the string unterminated, which is still an error. + * Whitespace inside a keyword is an error (`missing "=" after "us" in connection info string`) instead of becoming + part of the key. Whitespace around the `=` is unaffected. This most often shows up with an unquoted value + containing a space: `application_name=my app host=x` previously set neither parameter and sent `app host` to the + server as a runtime parameter, and now fails to parse. + + As with URIs, unrecognized keywords are still accepted where libpq rejects them, and an empty `user=` is still + dropped so that `PGUSER` and the OS user still apply. + +## Fixes + +* Keep the connection open after a recoverable PostgreSQL error from `Begin` or `BeginTx` + (Victor Alejandro Sanz Ararat) +* Call `TraceQueryEnd` when `Exec` fails while deallocating invalidated cached statements (Chris Bandy) +* Deallocate a failed prepare using the statement name actually sent to the server, and skip cleanup if Parse never + completed, avoiding leaked prepared statements and unnecessary cleanup errors (Eliran Ben-Zikri) +* Fix `LoadTypes` overwriting scalar codecs such as `box` and `point` with an incorrect `ArrayCodec` (Arsen Ozhetov) +* pgconn: retrieve field descriptions when cached descriptions are empty, such as for cursor `FETCH` statements, + including batch and pipeline execution (water) +* pgconn: keep batch statement descriptions and result formats aligned when commands return no rows or when + `Batch.ExecStatement` is mixed with other batch commands; preserve field descriptions for empty results +* pgconn: handle empty and comment-only queries in pipeline mode, discard stale statement data after bind errors, + and return a nil result from `Pipeline.GetResults` on error +* pgconn: skip reading the password file when a password is already set (Jared Fowkes) +* pgxpool: treat non-positive `MaxConnLifetime` values as unlimited instead of immediately expiring connections + (Aurelien Pillevesse) +* pgtype: support non-comma text array delimiters through `ArrayCodec.Delimiter`, including the semicolon delimiter + used by `box[]`. `LoadType` and `LoadTypes` now load the delimiter from PostgreSQL (Sueun Cho) +* pgtype: quote text array elements containing internal whitespace (Louisa Huang) +* pgtype: quote and escape text range bounds containing delimiters, quotes, or backslashes, and distinguish empty + string bounds from unbounded ranges (Sueun Cho) +* pgtype: preserve decimal precision in `Numeric.ScanScientific` and accept scientific notation in + `Numeric.UnmarshalJSON`; reject out-of-range scientific exponents and preserve the original input in parse errors + (Sueun Cho) +* pgtype: encode and decode numeric infinity in JSON as `"Infinity"` and `"-Infinity"` instead of encoding it as zero + (Vladimir Saraikin) +* pgtype: treat a valid `Numeric` with a nil `Int` as zero in `Int64Value`, and return errors when converting NaN or + infinity to an integer instead of panicking (Vladimir Saraikin) +* pgtype: fix an infinite loop when decoding binary numeric zero with a nonzero digit count (Vladimir Saraikin) +* pgtype: fix binary numeric digit-count overflow and trailing-byte handling. Binary encoding now rejects values + whose digit count, weight, or scale cannot fit the wire format, while accepting the full unsigned digit-count range. +* pgtype: fix scanning through multiple pointer levels, including SQL NULL and XML values, and return an error + instead of panicking when a pointer-to-pointer scan destination is nil (Rangel Reale) +* pgtype: use bounds-checked binary reads throughout the codecs and reject malformed lengths, counts, and trailing + data. This includes fixes for panics on malformed records and truncated multiranges (Vladimir Saraikin), and + validation of `bit` / `varbit` bit lengths against the actual data (g3m0sis). +* pgtype: return errors instead of panicking on malformed interval text (greymoth-jp), unterminated composite text + fields, and text arrays whose dimensions and element counts disagree +* pgtype: cap the initial allocation estimate when parsing hstore text to avoid excessive allocation from unvalidated + separator counts; valid hstores may still contain any number of pairs (AshSgDe29071999) +* pgtype: correct reversed bounds in integer scan error messages +* pgconn: a backslash as the last byte of a quoted value in a keyword/value connection string no longer panics with + `slice bounds out of range`. `host='a\` -- and the shorter `='\`, reachable through `pgx.ParseConfig` and + `pgxpool.ParseConfig` -- now return `unterminated quoted string in connection info string`, libpq's own message for + the same input. The unquoted branch has been guarded since be69c1c1; the quoted branch carried the same unguarded + increment since the parser was ported from pgx v3. Found by fuzzing (Maxim Korotkov) +* pgconn: error messages that embed the connection string now also redact `password` and `sslpassword` values supplied + as URI query parameters; previously only the userinfo password was redacted. Redaction matches keys the way the + parser does -- percent-encoded spellings such as `pass%77ord=` are recognized -- and masks the entire raw value, so + a password containing a space cannot leak its tail into the error message. Credentials stranded outside the + userinfo by a malformed URI are masked whole, and invalid-port errors no longer embed the offending text (which in + a malformed URI can be a mislaid password). Redaction of invalid connection strings is necessarily best effort: + their structure may be ambiguous, so some malformed inputs can still expose password text in an error. +* pgconn: `ParseConfigOptions.ConnStringAllowedKeys` no longer exempts an explicitly supplied empty port (`?port=` in + a URI or `port=` in a keyword/value string) from the allow-list. Only the implied all-empty port list of a + multi-host URI without ports (`postgres://h1,h2/db`) is exempt. An explicit empty port shadows `PGPORT` even though + it is empty, so it must be allowed like any other user-supplied key. The URI-only `ssl=true` alias is accepted when + either `ssl` or `sslmode` is allowed, and every `ssl`/`sslmode` spelling written in the URI is validated -- + including occurrences superseded by later repeated parameters. +* pgconn: drain socket before close in `asyncClose` so context cancellation produces a TCP FIN instead of RST, avoiding "connection reset by peer" on the server / proxy (Sean Chittenden at CrowdStrike, Inc.) +* pgproto3: `StartupMessage.Encode` rejects a NUL byte in any parameter name or value instead of writing it. The + startup message body is a run of NUL-delimited strings whose length is data-driven, so a NUL in a value ends that + parameter and everything after it is read by the server as further parameters -- an `application_name` of + `x\x00user\x00admin` changed the role the connection logged in as. libpq cannot reach this state because its + parameters are NUL-terminated C strings. `Connect` now fails with nothing written to the wire, which covers + settings that bypass connection string parsing: service files and direct assignment to `Config.RuntimeParams`, + `Config.User`, or `Config.Database`. +* pgconn: keyword/value connection strings containing a NUL byte are rejected by `ParseConfig`, as URIs already were. + # 5.10.0 (June 3, 2026) This release includes a significant amount of hardening against malicious or compromised PostgreSQL servers, diff --git a/vendor/github.com/jackc/pgx/v5/CLAUDE.md b/vendor/github.com/jackc/pgx/v5/CLAUDE.md index 71a8fc1..3b9f300 100644 --- a/vendor/github.com/jackc/pgx/v5/CLAUDE.md +++ b/vendor/github.com/jackc/pgx/v5/CLAUDE.md @@ -8,41 +8,59 @@ pgx is a PostgreSQL driver and toolkit for Go (`github.com/jackc/pgx/v5`). It pr ## Build & Test Commands +Every checkout has its own PostgreSQL 14-18 and CockroachDB instances, supervised by +process-compose. `mise run dev` starts PostgreSQL 18; tests start other targets on demand and stop +them afterwards unless they were explicitly prewarmed. See DEVELOPMENT.md. + ```bash -# Run all tests (requires PGX_TEST_DATABASE to be set) -go test ./... +mise run dev # start PostgreSQL 18 and the database supervisor +mise run dev:all # eagerly start every available database +mise run dev -- -D # ... detached; then `mise run dev:wait`, and + # `mise run dev:down` when finished. Agents must do this. + +./test.sh # Full suite against PostgreSQL 18 (the default target) +./test.sh pg16 # Against PostgreSQL 16 +./test.sh crdb # Against CockroachDB +./test.sh all # Every target (pg14-18 + crdb) +./test.sh pg16 -run TestConnect # Trailing arguments are passed to `go test` + +go test ./... # Also works: mise loads the default target's PGX_TEST_* +go test -race ./... # With the race detector + +goimports -w . # Format (always run after making changes) +golangci-lint run ./... # Lint + +mise run dev:ports # This checkout's ports and where each server's data lives +mise run db:start pg16 crdb # Prewarm targets; tests then leave them running +mise run db:stop pg16 crdb # Stop prewarmed targets +mise run db:psql # psql against PostgreSQL 18; `mise run db:psql 16` for another +process-compose process logs pg16 # One server's output +``` -# Run a specific test -go test -run TestFunctionName ./... +Do not hardcode database ports. They are allocated per checkout by port-tamer and read from the +environment (`PGPORT`, `PGPORT_16`, `CRDB_PORT`) or `.dev/ports.env`; 5432 and 26257 mean nothing +here. -# Run tests for a specific package -go test ./pgconn/... +The `PGX_TEST_*` connection strings have one definition, `scripts/lib/test_targets.rb`. Add or +change a target there, never in a second copy. -# Run tests with race detector -go test -race ./... +## Test Database Setup -# DevContainer: run tests against specific PostgreSQL versions -./test.sh pg18 # Default: PostgreSQL 18 -./test.sh pg16 -run TestConnect # Specific test against PG16 -./test.sh crdb # CockroachDB -./test.sh all # All targets (pg14-18 + crdb) +The lifecycle scripts handle setup: a PostgreSQL server initializes its cluster on first start and +creates `pgx_test` with the extensions and auth roles from `testsetup/postgresql_setup.sql`. +CockroachDB recreates `pgx_test` after each in-memory restart. Nothing needs to be set up by hand. -# Format (always run after making changes) -goimports -w . +Contributors who would rather point pgx at a PostgreSQL server they already have can set +`PGX_TEST_DATABASE` themselves; see CONTRIBUTING.md. Many tests are skipped unless additional +`PGX_TEST_*` variables are set (for TLS, SCRAM, MD5, unix socket, PgBouncer testing). -# Lint -golangci-lint run ./... -``` +## Reference Material -## Test Database Setup - -Tests require `PGX_TEST_DATABASE` environment variable. In the devcontainer, `test.sh` handles this. For local development: - -```bash -export PGX_TEST_DATABASE="host=localhost user=postgres password=postgres dbname=pgx_test" -``` +`references/` holds read-only reference checkouts used when building pgx — currently the PostgreSQL source tree pinned to `REL_18_STABLE`. It is gitignored and provisioned on demand: bare mirrors are cached at a machine-level path (`/persist/shared/references` in a devcontainer, `~/.local/share/pgx/references` natively; `REFERENCES_MIRROR_DIR` overrides) and lightweight local checkouts are created in `references/` with `rake references:setup`. Each checkout has per-instance Git metadata while borrowing the shared mirror's object store. Related tasks: `rake references:update`, `rake references:status`, `rake references:clean`. -The test database needs extensions: `hstore`, `ltree`, and a `uint64` domain. See `testsetup/postgresql_setup.sql` for full setup. Many tests are skipped unless additional `PGX_TEST_*` env vars are set (for TLS, SCRAM, MD5, unix socket, PgBouncer testing). +- Do not automatically provision or update `references/`. +- Never run `rake references:setup`, `rake references:update`, or any large download on your own initiative. +- If reference sources are missing, work without them or ask the user. ## Architecture diff --git a/vendor/github.com/jackc/pgx/v5/CONTRIBUTING.md b/vendor/github.com/jackc/pgx/v5/CONTRIBUTING.md index 2283ae6..c89ec1a 100644 --- a/vendor/github.com/jackc/pgx/v5/CONTRIBUTING.md +++ b/vendor/github.com/jackc/pgx/v5/CONTRIBUTING.md @@ -24,21 +24,30 @@ Using AI is acceptable (not that it can really be stopped) under one the followi ## Development Environment Setup -pgx tests naturally require a PostgreSQL database. It will connect to the database specified in the `PGX_TEST_DATABASE` -environment variable. The `PGX_TEST_DATABASE` environment variable can either be a URL or key-value pairs. In addition, -the standard `PG*` environment variables will be respected. Consider using [direnv](https://github.com/direnv/direnv) to -simplify environment variable handling. +pgx tests naturally require a PostgreSQL database. It will connect to the database specified in the +`PGX_TEST_DATABASE` environment variable. The `PGX_TEST_DATABASE` environment variable can either be +a URL or key-value pairs. In addition, the standard `PG*` environment variables will be respected. -### Devcontainer +### The full environment -The easiest way to start development is with the included devcontainer. It includes containers for each supported -PostgreSQL version as well as CockroachDB. `./test.sh all` will run the tests against all database types. +[DEVELOPMENT.md](DEVELOPMENT.md) describes the maintained setup: [mise](https://mise.jdx.dev) +installs the toolchain, and each checkout has its own PostgreSQL 14-18 clusters and CockroachDB +node, so the whole test matrix is available locally. PostgreSQL 18 stays running; other servers +start and stop around their tests. It works natively on macOS and Linux, and in the included +devcontainer. -### Using an Existing PostgreSQL Cluster Outside of a Devcontainer +``` +mise install +mise run dev:init +mise run dev +./test.sh all +``` -If you already have a PostgreSQL development server this is the quickest way to start and run the majority of the pgx -test suite. Some tests will be skipped that require server configuration changes (e.g. those testing different -authentication methods). +### Using an existing PostgreSQL cluster + +If you already have a PostgreSQL development server this is the quickest way to run the majority of +the pgx test suite, and it needs nothing from the section above. Some tests will be skipped that +require server configuration changes (e.g. those testing different authentication methods). Create and setup a test database: @@ -50,86 +59,30 @@ psql -c 'create extension ltree;' psql -c 'create domain uint64 as numeric(20,0);' ``` -Ensure a `postgres` user exists. This happens by default in normal PostgreSQL installs, but some installation methods -such as Homebrew do not. +Ensure a `postgres` user exists. This happens by default in normal PostgreSQL installs, but some +installation methods such as Homebrew do not. ``` createuser -s postgres ``` -Ensure your `PGX_TEST_DATABASE` environment variable points to the database you just created and run the tests. +Ensure your `PGX_TEST_DATABASE` environment variable points to the database you just created and run +the tests. ``` export PGX_TEST_DATABASE="host=/private/tmp database=pgx_test" go test ./... ``` -This will run the vast majority of the tests, but some tests will be skipped (e.g. those testing different connection methods). - -### Creating a New PostgreSQL Cluster Exclusively for Testing Outside of a Devcontainer - -The following environment variables need to be set both for initial setup and whenever the tests are run. (direnv is -highly recommended). Depending on your platform, you may need to change the host for `PGX_TEST_UNIX_SOCKET_CONN_STRING`. - -``` -export PGPORT=5015 -export PGUSER=postgres -export PGDATABASE=pgx_test -export POSTGRESQL_DATA_DIR=postgresql - -export PGX_TEST_DATABASE="host=127.0.0.1 database=pgx_test user=pgx_md5 password=secret" -export PGX_TEST_UNIX_SOCKET_CONN_STRING="host=/private/tmp database=pgx_test" -export PGX_TEST_TCP_CONN_STRING="host=127.0.0.1 database=pgx_test user=pgx_md5 password=secret" -export PGX_TEST_SCRAM_PASSWORD_CONN_STRING="host=127.0.0.1 user=pgx_scram password=secret database=pgx_test channel_binding=disable" -export PGX_TEST_SCRAM_PLUS_CONN_STRING="host=localhost user=pgx_ssl password=secret sslmode=verify-full sslrootcert=`pwd`/.testdb/ca.pem database=pgx_test channel_binding=require" -export PGX_TEST_MD5_PASSWORD_CONN_STRING="host=127.0.0.1 database=pgx_test user=pgx_md5 password=secret" -export PGX_TEST_PLAIN_PASSWORD_CONN_STRING="host=127.0.0.1 user=pgx_pw password=secret" -export PGX_TEST_TLS_CONN_STRING="host=localhost user=pgx_ssl password=secret sslmode=verify-full sslrootcert=`pwd`/.testdb/ca.pem channel_binding=disable" -export PGX_SSL_PASSWORD=certpw -export PGX_TEST_TLS_CLIENT_CONN_STRING="host=localhost user=pgx_sslcert sslmode=verify-full sslrootcert=`pwd`/.testdb/ca.pem database=pgx_test sslcert=`pwd`/.testdb/pgx_sslcert.crt sslkey=`pwd`/.testdb/pgx_sslcert.key" -``` - -Create a new database cluster. - -``` -initdb --locale=en_US -E UTF-8 --username=postgres .testdb/$POSTGRESQL_DATA_DIR - -echo "listen_addresses = '127.0.0.1'" >> .testdb/$POSTGRESQL_DATA_DIR/postgresql.conf -echo "port = $PGPORT" >> .testdb/$POSTGRESQL_DATA_DIR/postgresql.conf -cat testsetup/postgresql_ssl.conf >> .testdb/$POSTGRESQL_DATA_DIR/postgresql.conf -cp testsetup/pg_hba.conf .testdb/$POSTGRESQL_DATA_DIR/pg_hba.conf - -cd .testdb - -# Generate CA, server, and encrypted client certificates. -go run ../testsetup/generate_certs.go - -# Copy certificates to server directory and set permissions. -cp ca.pem $POSTGRESQL_DATA_DIR/root.crt -cp localhost.key $POSTGRESQL_DATA_DIR/server.key -chmod 600 $POSTGRESQL_DATA_DIR/server.key -cp localhost.crt $POSTGRESQL_DATA_DIR/server.crt - -cd .. -``` - - -Start the new cluster. This will be necessary whenever you are running pgx tests. - -``` -postgres -D .testdb/$POSTGRESQL_DATA_DIR -``` - -Setup the test database in the new cluster. - -``` -createdb -psql --no-psqlrc -f testsetup/postgresql_setup.sql -``` +This will run the vast majority of the tests, but some tests will be skipped (e.g. those testing +different connection methods). ### PgBouncer There are tests specific for PgBouncer that will be executed if `PGX_TEST_PGBOUNCER_CONN_STRING` is set. +The test PgBouncer must be version 1.21.0 or newer, use transaction pooling, and have `max_prepared_statements` set to a +non-zero value. This ensures the tests cover PgBouncer's protocol-level named prepared statement support in addition to +the pgx query modes that do not use named prepared statements. ### Optional Tests diff --git a/vendor/github.com/jackc/pgx/v5/DEVELOPMENT.md b/vendor/github.com/jackc/pgx/v5/DEVELOPMENT.md new file mode 100644 index 0000000..d2a91f8 --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/DEVELOPMENT.md @@ -0,0 +1,239 @@ +# Developing pgx + +Two supported environments, one command set. Native macOS or Linux is the fast inner loop; the +devcontainer is a Linux compatibility and isolation option. Both run the same mise tasks against +the same `process-compose.yaml`, so nothing here is specific to one of them except the +prerequisites in §1. + +```sh +scripts/setup-host # native macOS or Ubuntu host prerequisites (once per machine) +export PATH="$HOME/.local/bin:$PATH" # if mise was just installed +mise trust # trust this checkout's configuration +mise install # tool versions from mise.toml +mise run dev:init # this checkout's ports and certificates +mise run dev # start PostgreSQL 18 and the on-demand database supervisor +./test.sh # the suite against PostgreSQL 18 +./test.sh all # every target; non-default servers start and stop around their tests +``` + +--- + +## 1. Prerequisites + +### Native + +[mise](https://mise.jdx.dev) provides Go, Ruby, CockroachDB, process-compose, and port-tamer. +PostgreSQL itself is the one thing it does not: pgx tests against five major versions, and they +come from the system package manager. + +```sh +scripts/setup-host +``` + +On macOS, install [Homebrew](https://brew.sh) first. The script uses `Brewfile` to install +PostgreSQL 14-18 and Ruby build dependencies. On Ubuntu, it uses sudo when needed, configures +the official PostgreSQL apt repository, and installs the same server versions and build +dependencies. Other Linux distributions require manual prerequisite installation. + +The Ubuntu installer temporarily disables automatic `main` cluster creation during server +installation and removes that override on exit, leaving the package-owned cluster configuration +unchanged. The macOS installer does not start Homebrew services. + +The dispatcher installs mise if it is missing and prints its executable path. It does not run +`mise trust`, install project tools, allocate ports, initialize databases, or start servers. Run +the remaining commands above explicitly as your regular development user. + +- **Do not** `brew services start` any of them, and on Debian do not let the packages create a + machine-wide cluster. pgx runs its own clusters per checkout. +- The Homebrew formulas are *keg-only*, so `psql` and `pg_isready` are not on your `PATH` after + install. `mise.toml` adds PostgreSQL 18's `bin` to `PATH` for this project — one client serves + every server, since libpq is backward compatible. +- Only the majors you actually test against need to be installed. `mise run dev` looks for each + one's server binaries and names any that are missing; `./test.sh pg15` reports a direct install + hint rather than trying to launch a missing server. `PGBIN_16` and friends override the search + for a major built or installed somewhere unusual. +- Clusters are created with the `en_US.UTF-8` locale where the system has it (matching CI and the + container images this replaced), falling back to `C.UTF-8` and then `C`. A stock Debian or Ubuntu + has only `C.UTF-8` unless you have run `locale-gen en_US.UTF-8`. + +### Devcontainer + +Reopen in the container; `.devcontainer/` handles the rest. It installs the same five PostgreSQL +servers and runs the same per-checkout clusters — it is a Linux shell around this same setup, not a +second architecture. + +--- + +## 2. Checkouts are the unit of isolation + +A git worktree is the native equivalent of a second devcontainer instance. Each one gets: + +``` +.dev/ # gitignored, per-checkout runtime state + ports.env # this checkout's TCP ports (port-tamer's state file) + derived.env # PG* defaults and the default target's PGX_TEST_* set + certs/ # the client certificates the TLS tests use + logs/ # one log per service + -/postgres/run # one socket directory, shared by all five servers (mode 0700) + -/postgres//data + -/crdb/ +``` + +The last three — the clusters, the CockroachDB store and the socket directory — move when +`PGX_DEV_RUNTIME_DIR` is set. The devcontainer sets it to `/persist/local/dev`, on its own named +volume: `.dev/` is inside the `/workspaces/pgx` bind mount, and `initdb` and Unix sockets do not +work reliably on Docker Desktop's macOS/Windows file sharing. (The per-version PostgreSQL +containers this setup replaced used named volumes for their data and socket directories for the +same reason.) Everything else stays in the checkout. + +```sh +git worktree add ../pgx-feature-x feature-x +cd ../pgx-feature-x +mise run dev:init && mise run dev +``` + +Both checkouts run simultaneously: different ports, independent database state, independent +process-compose instances. `mise run dev:ports` prints the allocation. + +Ports are allocated once per checkout by [port-tamer](https://github.com/jackc/port-tamer) and then +persisted. `port-tamer.toml` declares which ports a checkout needs — **append new entries at the +end**, since inserting or reordering renumbers the existing ones. A listening port never moves an +existing allocation, because it may well belong to this checkout's own running services; when two +checkouts genuinely collide, stop one and run `mise run dev:ports:overwrite`. + +The data directories are keyed by platform because a cluster `initdb`'d by the Linux devcontainer +cannot be read by a native macOS server, and one checkout may be opened both ways. After switching +a checkout between the two, re-run `mise run dev:ports:ensure` so the derived paths follow. + +**Reference sources** (`references/`) are provisioned per checkout with `rake references:setup`, +sharing one machine-level mirror (`/persist/shared` in a container, `~/.local/share/pgx` natively; +`REFERENCES_MIRROR_DIR` overrides). They are a multi-GB download; nothing provisions them +automatically. + +--- + +## 3. The databases + +`mise run dev` starts PostgreSQL 18 under +[process-compose](https://github.com/F1bonacc1/process-compose). PostgreSQL 14-17 and a single-node +in-memory CockroachDB are registered with the same supervisor but disabled initially, ready to be +started by a test or an explicit `db:start`. `mise run dev:all` eagerly starts every available +server. These are the same services the devcontainer used to run as containers; they are now +ordinary processes against this checkout's own clusters. + +Each PostgreSQL server initializes its cluster on first start — `initdb`, this project's +`pg_hba.conf`, and the TLS certificates — then creates `pgx_test` with its extensions and its +`pgx_md5` / `pgx_scram` / `pgx_pw` / `pgx_ssl` / `pgx_sslcert` roles. The default server uses its +follow-on `pg18-setup` process; the lazy lifecycle performs the same idempotent setup directly for +an on-demand target. Restarts are free once the cluster exists. If setup fails partway it drops the +half-built database so the next start retries rather than reporting it as present. + +CockroachDB has no separate setup process: its store is in memory, so every restart is an empty +cluster and its readiness probe creates `pgx_test` itself. + +```sh +process-compose process list # status, scriptable +process-compose process logs pg16 # one server's output +mise run db:start pg16 crdb # prewarm one or more targets +mise run db:stop pg16 crdb # stop them again +mise run db:start all # prewarm every target +process-compose down # stop this checkout's stack only +mise run db:psql # psql against the already-running PostgreSQL 18 +mise run db:start pg16 # prewarm PostgreSQL 16 before interactive use +mise run db:psql 16 # psql against PostgreSQL 16 +mise run db:psql 16 -c 'select 1' # arguments after the major go to psql +``` + +The `process-compose` commands need no flags: `PC_PORT_NUM` is part of this checkout's +environment, so they never reach another checkout's stack. + +`rake db:psql[16]` is the same as `mise run db:psql 16`. Note the brackets — a bare +`rake db:psql 16` is rake asking for a *task* named `16`, not an argument, so the `db:*` tasks +reject it rather than quietly acting on every cluster. + +`mise run db:reset` destroys and re-creates clusters and refuses to run while the selected servers +are up: removing a data directory under a live postmaster corrupts it. Stop selected targets with +`mise run db:stop`, or stop the whole supervisor with `mise run dev:down`. + +All five PostgreSQL servers share **one** Unix socket directory. Sockets are named +`.s.PGSQL.`, so the port picks the server — which is what lets a single +`PGX_TEST_UNIX_SOCKET_CONN_STRING` work for every major, exactly as the container's shared +`/var/run/postgresql` volume did. + +Tests preserve explicit choices: if `db:start` prewarmed a target, tests leave it running. If a +test had to start the target, it stops it in an `ensure` block whether the suite passes, fails, or +is interrupted. A per-target lock prevents concurrent test commands from stopping a server the +other is using. + +--- + +## 4. Running tests + +```sh +./test.sh # PostgreSQL 18, the default target +./test.sh pg14 # PostgreSQL 14 +./test.sh crdb # CockroachDB +./test.sh all # every target, sequentially +./test.sh pg16 -run TestConnect # trailing arguments go to `go test` +``` + +`mise run test` and `mise run test:all` are equivalent. All of them require the stack to be +running, but only their selected database target needs to be up. A stopped target is started, +bootstrapped, and stopped automatically. `./test.sh all` does this sequentially, so at most the +default PostgreSQL 18 plus one additional database is normally resident. + +A bare `go test ./...` also works: mise loads `.dev/derived.env`, which carries the default +target's full `PGX_TEST_*` set, so an activated shell is already pointed at PostgreSQL 18. + +Those connection strings have exactly one definition, `scripts/lib/test_targets.rb`. Both consumers +— the generated `.dev/derived.env` and `./test.sh ` — read it, so there is no second copy +to drift. + +Some tests only run when their environment variable is set. `go test ./... -v | grep SKIP` shows +what is being skipped; on a healthy stack that is the PgBouncer, OAuth, CrateDB, and libpq-oracle +tests, which are CI-only or manual. + +--- + +## 5. Everyday commands + +| Command | What it does | +|---|---| +| `mise run dev` | start PostgreSQL 18 and the on-demand database supervisor | +| `mise run dev:all` | eagerly start every available database | +| `mise run dev -- -D` | start the default stack detached, for CI and agents | +| `mise run dev:wait` / `dev:down` | wait for a detached stack / stop it | +| `mise run dev:init` | allocate this checkout's ports, decode its certificates | +| `mise run dev:ports` | the allocation, and where each server's data lives | +| `mise run test [target]` | the suite against one target | +| `mise run test:all` | every target | +| `mise run db:start [targets...]` / `db:stop` | prewarm or stop targets (`pg16`, `crdb`, or `all`) | +| `mise run db:init [major]` / `db:psql [major]` / `db:reset [major]` | create / open / rebuild the clusters | +| `mise run fmt` | `goimports -w .` | +| `mise run generate` | regenerate the ERB-templated sources | + +`rake ` remains equally valid and is where some of the logic lives; `rake -T` lists +everything, including tasks with no mise wrapper (`references:*`, `db:setup`). + +--- + +## 6. Notes + +- **`PGHOST` and `PGPORT` are a pair.** They come from `.dev/` via mise — `PGPORT` from the + allocation, `PGHOST` derived from it. Setting one without the other names a real port on the + wrong server, and the error will point at a socket nothing ever created. `mise run dev` asserts + the two agree with the cluster before starting anything. +- **`.dev/derived.env` is generated.** If it is ever hand-edited into something mise's dotenv + parser rejects, every `mise` command in the directory fails — including the one that would + rewrite it. Recover with `rm .dev/derived.env && mise run dev:ports:ensure`. +- **A `~/.psqlrc` that changes session defaults** can break the database bootstrap while leaving + interactive use fine. The scripts pass `--no-psqlrc` for that reason. +- **`./test.sh` needs Ruby 3.0+**, which `mise install` provides. If mise is installed but not yet + activated in your shell, `./test.sh` re-runs itself through `mise exec` rather than failing on + the system interpreter. +- **CI does not use any of this.** `.github/workflows/ci.yml` installs system PostgreSQL through + `ci/setup_test.bash` and carries its own copy of the connection strings. Local runs and CI + exercise the same auth paths — `PGX_TEST_DATABASE` connects as `pgx_md5`, and one + `testsetup/pg_hba.conf` serves both — but the two setups are still independent. +- **PgBouncer and the PG18 OAuth validator module** are exercised only in CI. Neither is part of + the local stack, and both were absent from the devcontainer too. diff --git a/vendor/github.com/jackc/pgx/v5/README.md b/vendor/github.com/jackc/pgx/v5/README.md index aa35e4a..4dd8ddd 100644 --- a/vendor/github.com/jackc/pgx/v5/README.md +++ b/vendor/github.com/jackc/pgx/v5/README.md @@ -12,7 +12,15 @@ The toolkit component is a related set of packages that implement PostgreSQL fun and type mapping between PostgreSQL and Go. These underlying packages can be used to implement alternative drivers, proxies, load balancers, logical replication clients, etc. -## Example Usage +## Quick Start + +### Installation + +```bash +go get github.com/jackc/pgx/v5 +``` + +### Example Usage ```go package main @@ -46,7 +54,18 @@ func main() { } ``` -See the [getting started guide](https://github.com/jackc/pgx/wiki/Getting-started-with-pgx) for more information. +### Connection Configuration + +`pgx.Connect` and `pgxpool.New` accept PostgreSQL connection URLs (such as `postgres://user:pass@host:5432/db?sslmode=verify-full`) as well as `key=value` strings. See [`pgconn.ParseConfig`](https://pkg.go.dev/github.com/jackc/pgx/v5/pgconn#ParseConfig) and the [PostgreSQL connection string documentation](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) for supported options and environment variables. + +For a step-by-step walkthrough, see the [getting started guide](https://github.com/jackc/pgx/wiki/Getting-started-with-pgx). + +## Documentation + +Package documentation and API reference are available on [pkg.go.dev](https://pkg.go.dev/github.com/jackc/pgx/v5): +* [`pgx`](https://pkg.go.dev/github.com/jackc/pgx/v5) — base PostgreSQL driver +* [`pgxpool`](https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool) — concurrency-safe connection pool +* [`stdlib`](https://pkg.go.dev/github.com/jackc/pgx/v5/stdlib) — `database/sql` compatibility adapter ## Features @@ -82,9 +101,30 @@ The pgx interface is recommended when: It is also possible to use the `database/sql` interface and convert a connection to the lower-level pgx interface as needed. -## Testing +## Development and Testing + +Each checkout has its own PostgreSQL 14-18 clusters and CockroachDB node, so the whole test matrix +is available locally on macOS, on Linux, or in the included devcontainer. Only PostgreSQL 18 stays +running by default; other targets start and stop around their tests: + +```sh +scripts/setup-host # native macOS (Homebrew required) or Ubuntu: host packages and mise +export PATH="$HOME/.local/bin:$PATH" # if mise was just installed +mise trust +mise install # project tools +mise run dev:init # checkout ports and certificates +mise run dev # start PostgreSQL 18 and the on-demand database supervisor +./test.sh # the suite against PostgreSQL 18 +./test.sh all # every target, starting and stopping each server as needed +``` + +Host setup installs PostgreSQL 14-18 and Ruby build dependencies. It does not install project +tools or initialize the checkout; those remain separate steps above. The devcontainer already +provides the host prerequisites. -See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions. +See [DEVELOPMENT.md](./DEVELOPMENT.md) for the full setup, and +[CONTRIBUTING.md](./CONTRIBUTING.md) for how to contribute — including how to run the tests against +a PostgreSQL server you already have, without any of the above. ## Architecture diff --git a/vendor/github.com/jackc/pgx/v5/Rakefile b/vendor/github.com/jackc/pgx/v5/Rakefile index 3e3aa50..57c6050 100644 --- a/vendor/github.com/jackc/pgx/v5/Rakefile +++ b/vendor/github.com/jackc/pgx/v5/Rakefile @@ -1,4 +1,5 @@ require "erb" +require "fileutils" rule '.go' => '.go.erb' do |task| erb = ERB.new(File.read(task.source)) @@ -16,3 +17,293 @@ generated_code_files = [ desc "Generate code" task generate: generated_code_files + +# dev -- per-checkout development state. A git worktree is the native equivalent of a second +# devcontainer instance: same machine, same network stack, so the five PostgreSQL servers and +# CockroachDB that each owned a well-known port in their own container namespace now need distinct +# ones. `dev:ports:*` drives port-tamer (port-tamer.toml declares the names; the allocation lands +# in the gitignored .dev/ports.env) and writes the values derived from it -- scripts/devenv.rb. +namespace :dev do + namespace :ports do + desc "Allocate this checkout's TCP ports if it has none (idempotent)" + task :ensure do + sh RbConfig.ruby, "scripts/devenv.rb", "ensure" + end + + desc "Move this checkout to a different port group (stop its services first)" + task :overwrite do + sh RbConfig.ruby, "scripts/devenv.rb", "overwrite" + end + + desc "Print this checkout's port allocation and where each server's data lives" + task :show do + sh RbConfig.ruby, "scripts/devenv.rb", "show" + end + end +end + +# db -- this checkout's own PostgreSQL clusters, one per major (scripts/devdb.rb). Clusters per +# checkout rather than databases inside shared servers: destructive resets stay local, two +# checkouts run at once, and the layout matches the per-instance devcontainer model it replaces. +# The SERVERS run in the foreground under process-compose (process-compose.yaml); these are the +# one-shot half. Each task takes an optional major, defaulting to all of them. +# +# rake db:init initialize every cluster +# rake db:init[16] just PostgreSQL 16 +# rake db:psql[16] psql against PostgreSQL 16 (default: the newest major) +# +# The mise wrappers (`mise run db:init 16`) call scripts/devdb.rb directly rather than routing +# through these tasks — mise APPENDS its arguments, and `rake db:init 16` means "run task db:init, +# then run task 16", not "run db:init with the argument 16". +namespace :db do + # A bare major on the command line is the natural thing to type and the one rake cannot mean: + # it becomes a second TASK name, so `rake db:reset 16` runs db:reset with no major — every + # cluster — and only then fails on the unknown task, after the data is gone. Catch it first. + def db_major(task, args) + stray = Rake.application.top_level_tasks.find { |t| t =~ /\A\d+\z/ } + if stray + abort "rake: `#{stray}` is being read as a task name, not an argument. " \ + "Write `rake #{task.name}[#{stray}]` (or `mise run #{task.name} #{stray}`)." + end + + Array(args[:major]) + end + + desc "Create this checkout's PostgreSQL clusters under .dev (idempotent). Optional: rake db:init[16]" + task :init, [:major] do |task, args| + sh RbConfig.ruby, "scripts/devdb.rb", "init", *db_major(task, args) + end + + desc "psql against one of this checkout's clusters. Optional: rake db:psql[16]" + task :psql, [:major] do |task, args| + sh RbConfig.ruby, "scripts/devdb.rb", "psql", *db_major(task, args) + end + + desc "Create the pgx_test database and roles in a running cluster (idempotent)" + task :setup, [:major] do |task, args| + sh RbConfig.ruby, "scripts/devdb.rb", "setup", *db_major(task, args) + end + + desc "Destroy this checkout's clusters and re-initialize (destructive; CONFIRM=yes)" + task :reset, [:major] do |task, args| + sh RbConfig.ruby, "scripts/devdb.rb", "reset", *db_major(task, args) + end +end + +# references:* — provision local, read-only checkouts of reference material used +# when building pgx (currently the PostgreSQL source tree). +# +# Storage model: +# * A bare `--mirror` clone of each repo lives on the devcontainer's shared +# persist volume under MIRROR_ROOT. It holds the full history + all +# branches/tags, is downloaded once, and survives container rebuilds. It is +# the canonical copy, shared across every container for this project. +# * Each container creates a lightweight shared clone in ./references/. +# Its Git metadata is local to that checkout while its object store borrows +# from the mirror. Containers can therefore use the same in-container path +# without sharing Git worktree registrations or downloading history again. +# +# Provisioning a new container is therefore cheap: a local clone from the +# already-present mirror, with no network fetch or duplicate object store. + +# Each entry is one reference repo. `ref` is the branch/tag checked out into the +# local checkout. PostgreSQL is pinned to REL_18_STABLE to match the devcontainer's +# default PG18 test target. +REFERENCE_REPOS = [ + { name: "postgres", url: "https://github.com/postgres/postgres.git", ref: "REL_18_STABLE", license: "PostgreSQL License" }, +].freeze + +# Where the canonical bare mirrors live. They are machine-level state, not checkout-level: +# multiple GB, read-only, and identical for every checkout -- so they belong outside the tree, and +# every checkout on the machine shares one copy. +# +# * devcontainer -- the shared persist volume, so every container for this project reuses one +# download and a rebuild costs nothing. +# * native -- an XDG data directory under $HOME, where git worktrees of this repo share it the +# same way containers share the volume. +# +# REFERENCES_MIRROR_DIR overrides both (a second disk, a scratch location, a test). +def default_mirror_root + return "/persist/shared/references" if File.directory?("/persist/shared") + + xdg = ENV["XDG_DATA_HOME"] + base = xdg.nil? || xdg.empty? ? File.join(Dir.home, ".local", "share") : xdg + File.join(base, "pgx", "references") +end + +MIRROR_ROOT = ENV.fetch("REFERENCES_MIRROR_DIR") { default_mirror_root } +CHECKOUT_ROOT = File.join(__dir__, "references") + +def mirror_path(repo) = File.expand_path(File.join(MIRROR_ROOT, "#{repo[:name]}.git")) +def checkout_path(repo) = File.join(CHECKOUT_ROOT, repo[:name]) + +# File.exist? is false for dangling symlinks, which are still leftovers setup +# must replace (and clean must remove). +def path_present?(path) = File.exist?(path) || File.symlink?(path) + +# Run a git command against a bare mirror. Names the gitdir explicitly and lifts +# any safe.bareRepository=explicit guard. Raises on failure. +def git_bare(repo, *args) + sh "git", "-c", "safe.bareRepository=all", "--git-dir", mirror_path(repo), *args +end + +# Capture stdout of a command given as an argv array (no shell parsing, no +# quoting pitfalls). Returns [stdout_string, success_boolean]. +def capture(*args) + out = IO.popen(args, err: File::NULL, &:read) + [out.to_s, $?.success?] +end + +# A managed checkout has its own repository metadata, uses this exact mirror as +# its origin, and borrows that mirror's object database. This rejects ordinary +# clones, linked worktrees left by the old implementation, and dangling gitfiles. +def checkout_valid?(repo) + wp = checkout_path(repo) + git_dir = File.join(wp, ".git") + objects_dir = File.join(git_dir, "objects") + alternates_path = File.join(objects_dir, "info", "alternates") + + return false unless File.directory?(git_dir) && File.file?(alternates_path) + + inside, inside_ok = capture("git", "-C", wp, "rev-parse", "--is-inside-work-tree") + origin, origin_ok = capture("git", "-C", wp, "remote", "get-url", "origin") + return false unless inside_ok && inside.strip == "true" && origin_ok + return false unless File.identical?(File.expand_path(origin.strip, wp), mirror_path(repo)) + + File.foreach(alternates_path).any? do |alternate| + alternate = alternate.strip + next false if alternate.empty? + + File.identical?(File.expand_path(alternate, objects_dir), File.join(mirror_path(repo), "objects")) + end +rescue SystemCallError + false +end + +# A mirror is valid only if it exists AND has at least one ref — this rejects a +# directory left behind by an interrupted clone (which exists but is incomplete). +def mirror_valid?(repo) + return false unless File.directory?(mirror_path(repo)) + out, ok = capture("git", "-c", "safe.bareRepository=all", "--git-dir", mirror_path(repo), "for-each-ref", "--count=1") + ok && !out.strip.empty? +end + +# Clone the bare mirror onto the persist volume if it is missing or broken. +def ensure_mirror(repo) + if mirror_valid?(repo) + puts " mirror cached: #{mirror_path(repo)}" + else + FileUtils.rm_rf(mirror_path(repo)) # clear any partial/broken clone + puts " cloning mirror (full history): #{repo[:url]}" + sh "git", "clone", "--mirror", repo[:url], mirror_path(repo) + end +end + +# Serialize mirror creation and updates across devcontainer instances. Local +# checkouts do not share metadata, but they all read from the same object store. +def with_mirror_lock(repo) + FileUtils.mkdir_p(MIRROR_ROOT) + File.open(File.join(MIRROR_ROOT, ".#{repo[:name]}.lock"), File::RDWR | File::CREAT, 0o644) do |lock| + lock.flock(File::LOCK_EX) + yield + end +end + +# Check out (or re-point) the local clone at the configured ref. Resolve the +# commit in the freshly updated mirror so an existing checkout cannot remain +# stale merely because only the mirror was fetched. +def ensure_checkout(repo) + wp = checkout_path(repo) + ref = repo[:ref] + + commit, ok = capture("git", "-c", "safe.bareRepository=all", "--git-dir", mirror_path(repo), + "rev-parse", "--verify", "#{ref}^{commit}") + raise "ref #{ref.inspect} not found in #{mirror_path(repo)}" unless ok && !commit.strip.empty? + + if checkout_valid?(repo) + puts " checkout present: #{wp} -> #{ref}" + else + puts " replacing invalid checkout: #{wp}" if path_present?(wp) + FileUtils.rm_rf(wp) if path_present?(wp) + puts " cloning checkout: #{wp} -> #{ref}" + sh "git", "clone", "--shared", "--no-checkout", mirror_path(repo), wp + raise "checkout at #{wp} is not linked to #{mirror_path(repo)}" unless checkout_valid?(repo) + end + + sh "git", "-C", wp, "checkout", "--detach", commit.strip +end + +# Run a block per repo, collecting failures so one bad repo does not abort the rest. +def for_each_repo + failures = [] + REFERENCE_REPOS.each do |repo| + puts "#{repo[:name]}:" + begin + yield repo + rescue => e + warn " FAILED: #{e.message}" + failures << repo[:name] + end + end + abort "references: failed for #{failures.join(', ')}" unless failures.empty? +end + +namespace :references do + desc "Clone/refresh reference mirrors on persist and create checkouts in references/" + task :setup do + FileUtils.mkdir_p(MIRROR_ROOT) + FileUtils.mkdir_p(CHECKOUT_ROOT) + for_each_repo do |repo| + with_mirror_lock(repo) do + ensure_mirror(repo) + ensure_checkout(repo) + end + end + puts + puts "Done. Reference sources are in #{CHECKOUT_ROOT}" + Rake::Task["references:status"].invoke + end + + desc "Fetch latest upstream for all mirrors and re-point checkouts" + task :update do + for_each_repo do |repo| + with_mirror_lock(repo) do + abort "mirror missing for #{repo[:name]}; run `rake references:setup`" unless mirror_valid?(repo) + git_bare(repo, "remote", "update", "--prune") + ensure_checkout(repo) + end + end + end + + desc "Show provisioned reference repos, their pinned ref, and current HEAD" + task :status do + puts + puts format(" %-16s %-14s %-14s %s", "REPO", "REF", "HEAD", "LICENSE") + REFERENCE_REPOS.each do |repo| + wp = checkout_path(repo) + state = + if checkout_valid?(repo) + head, ok = capture("git", "-C", wp, "rev-parse", "--short", "HEAD") + ok && !head.strip.empty? ? head.strip : "(invalid)" + elsif path_present?(wp) + "(invalid)" + else + "(not set up)" + end + puts format(" %-16s %-14s %-14s %s", repo[:name], repo[:ref], state, repo[:license]) + end + puts + puts " mirrors: #{MIRROR_ROOT}" + puts " checkouts: #{CHECKOUT_ROOT}" + end + + desc "Remove checkouts from references/ (keeps the cached mirrors on persist)" + task :clean do + REFERENCE_REPOS.each do |repo| + wp = checkout_path(repo) + next unless path_present?(wp) + puts "removing checkout: #{wp}" + FileUtils.rm_rf(wp) + end + end +end diff --git a/vendor/github.com/jackc/pgx/v5/conn.go b/vendor/github.com/jackc/pgx/v5/conn.go index bc5d064..1e60af2 100644 --- a/vendor/github.com/jackc/pgx/v5/conn.go +++ b/vendor/github.com/jackc/pgx/v5/conn.go @@ -36,9 +36,10 @@ type ConnConfig struct { DescriptionCacheCapacity int // DefaultQueryExecMode controls the default mode for executing queries. By default pgx uses the extended protocol - // and automatically prepares and caches prepared statements. However, this may be incompatible with proxies such as - // PGBouncer. In this case it may be preferable to use [QueryExecModeExec] or [QueryExecModeSimpleProtocol]. The same - // functionality can be controlled on a per query basis by passing a [QueryExecMode] as the first query argument. + // and automatically prepares and caches prepared statements. This may be incompatible with proxies such as PgBouncer + // unless they are configured to support protocol-level prepared statements. In an incompatible configuration it may + // be preferable to use [QueryExecModeExec] or [QueryExecModeSimpleProtocol]. The same functionality can be controlled + // on a per query basis by passing a [QueryExecMode] as the first query argument. DefaultQueryExecMode QueryExecMode createdByParseConfig bool // Used to enforce created by ParseConfig rule. @@ -358,8 +359,13 @@ func (c *Conn) Prepare(ctx context.Context, name, sql string) (sd *pgconn.Statem sd, err = c.pgConn.Prepare(ctx, psName, sql, nil) if err != nil { var pErr *pgconn.PrepareError - if errors.As(err, &pErr) { - c.failedDescribeStatement = psKey + if errors.As(err, &pErr) && pErr.ParseComplete { + // The server-side statement was created under psName — the name sent in + // Parse. In the name == sql case psKey is the SQL text, and deallocating + // by it would close a nonexistent statement while leaking the real one. + // When Parse never completed no statement was created at all, so there + // is nothing to clean up. + c.failedDescribeStatement = psName } return nil, err } @@ -475,6 +481,9 @@ func (c *Conn) Exec(ctx context.Context, sql string, arguments ...any) (pgconn.C } if err := c.deallocateInvalidatedCachedStatements(ctx); err != nil { + if c.queryTracer != nil { + c.queryTracer.TraceQueryEnd(ctx, c, TraceQueryEndData{Err: err}) + } return pgconn.CommandTag{}, err } @@ -742,7 +751,7 @@ type QueryRewriter interface { // collected before processing rather than processed while receiving each row. This avoids the possibility of the // application processing rows from a query that the server rejected. The CollectRows function is useful here. // -// An implementor of QueryRewriter may be passed as the first element of args. It can rewrite the sql and change or +// An implementer of QueryRewriter may be passed as the first element of args. It can rewrite the sql and change or // replace args. For example, NamedArgs is QueryRewriter that implements named arguments. // // For extra control over how the query is executed, the types QueryExecMode, QueryResultFormats, and @@ -1305,7 +1314,7 @@ func (c *Conn) LoadType(ctx context.Context, typeName string) (*pgtype.Type, err switch typtype { case "b": // array - elementOID, err := c.getArrayElementOID(ctx, oid) + elementOID, delimiter, err := c.getArrayElementOIDAndDelimiter(ctx, oid) if err != nil { return nil, err } @@ -1315,7 +1324,7 @@ func (c *Conn) LoadType(ctx context.Context, typeName string) (*pgtype.Type, err return nil, errors.New("array element OID not registered") } - return &pgtype.Type{Name: typeName, OID: oid, Codec: &pgtype.ArrayCodec{ElementType: dt}}, nil + return &pgtype.Type{Name: typeName, OID: oid, Codec: &pgtype.ArrayCodec{ElementType: dt, Delimiter: delimiter}}, nil case "c": // composite fields, err := c.getCompositeFields(ctx, oid) if err != nil { @@ -1361,15 +1370,16 @@ func (c *Conn) LoadType(ctx context.Context, typeName string) (*pgtype.Type, err } } -func (c *Conn) getArrayElementOID(ctx context.Context, oid uint32) (uint32, error) { +func (c *Conn) getArrayElementOIDAndDelimiter(ctx context.Context, oid uint32) (uint32, byte, error) { var typelem uint32 + var typdelim string - err := c.QueryRow(ctx, "select typelem from pg_type where oid=$1", oid).Scan(&typelem) + err := c.QueryRow(ctx, "select typelem, typdelim::text from pg_type where oid=$1", oid).Scan(&typelem, &typdelim) if err != nil { - return 0, err + return 0, 0, err } - return typelem, nil + return typelem, parseTypeDelimiter(typdelim), nil } func (c *Conn) getRangeElementOID(ctx context.Context, oid uint32) (uint32, error) { diff --git a/vendor/github.com/jackc/pgx/v5/derived_types.go b/vendor/github.com/jackc/pgx/v5/derived_types.go index 3916006..e588c7c 100644 --- a/vendor/github.com/jackc/pgx/v5/derived_types.go +++ b/vendor/github.com/jackc/pgx/v5/derived_types.go @@ -64,9 +64,12 @@ UNION ALL -- As can be seen, there are 3 ways this can occur (the last of which -- is due to being a composite class, where the composite fields are children) pc(parent, child) AS ( + -- typtype = 'b' AND typelem != 0 is not sufficient to identify an array type: some + -- scalar types (box, point, line, lseg, name, ...) also set typelem to describe their + -- internal C representation. typcategory = 'A' is the reliable "is an array" signal. SELECT parent.oid, parent.typelem FROM pg_type parent - WHERE parent.typtype = 'b' AND parent.typelem != 0 + WHERE parent.typtype = 'b' AND parent.typelem != 0 AND parent.typcategory = 'A' UNION ALL SELECT parent.oid, parent.typbasetype FROM pg_type parent @@ -113,6 +116,7 @@ SELECT typname, typtype, typbasetype, typelem, + typdelim, pg_type.oid,`) if supportsMultirange { parts = append(parts, ` @@ -135,9 +139,13 @@ SELECT typname, parts = append(parts, ` LEFT OUTER JOIN composite USING (oid) LEFT OUTER JOIN pg_namespace ON (pg_type.typnamespace = pg_namespace.oid) - WHERE NOT (typtype = 'b' AND typelem = 0)`) + -- Only emit typtype = 'b' rows that are true arrays (typcategory = 'A'). Other base + -- types, including ones with a non-zero typelem for non-array reasons (box, point, + -- line, lseg, name, ...), already have a codec registered and must not be re-emitted, + -- or LoadTypes will overwrite their correct codec with a bogus ArrayCodec. + WHERE NOT (typtype = 'b' AND NOT (typelem != 0 AND typcategory = 'A'))`) parts = append(parts, ` - GROUP BY typname, pg_namespace.nspname, typtype, typbasetype, typelem, pg_type.oid, pg_range.rngsubtype,`) + GROUP BY typname, pg_namespace.nspname, typtype, typbasetype, typelem, typdelim, pg_type.oid, pg_range.rngsubtype,`) if supportsMultirange { parts = append(parts, ` multirange.rngtypid,`) @@ -150,11 +158,18 @@ SELECT typname, type derivedTypeInfo struct { Oid, Typbasetype, Typelem, Rngsubtype, Rngtypid uint32 - TypeName, Typtype, NspName string + TypeName, Typtype, NspName, Typdelim string Attnames []string Atttypids []uint32 } +func parseTypeDelimiter(typdelim string) byte { + if typdelim == "" { + return 0 + } + return typdelim[0] +} + // LoadTypes performs a single (complex) query, returning all the required // information to register the named types, as well as any other types directly // or indirectly required to complete the registration. @@ -177,7 +192,7 @@ func (c *Conn) LoadTypes(ctx context.Context, typeNames []string) ([]*pgtype.Typ result := make([]*pgtype.Type, 0, 100) for rows.Next() { ti := derivedTypeInfo{} - err = rows.Scan(&ti.TypeName, &ti.NspName, &ti.Typtype, &ti.Typbasetype, &ti.Typelem, &ti.Oid, &ti.Rngtypid, &ti.Rngsubtype, &ti.Attnames, &ti.Atttypids) + err = rows.Scan(&ti.TypeName, &ti.NspName, &ti.Typtype, &ti.Typbasetype, &ti.Typelem, &ti.Typdelim, &ti.Oid, &ti.Rngtypid, &ti.Rngsubtype, &ti.Attnames, &ti.Atttypids) if err != nil { return nil, fmt.Errorf("While scanning type information: %w", err) } @@ -188,7 +203,7 @@ func (c *Conn) LoadTypes(ctx context.Context, typeNames []string) ([]*pgtype.Typ if !ok { return nil, fmt.Errorf("Array element OID %v not registered while loading pgtype %q", ti.Typelem, ti.TypeName) } - type_ = &pgtype.Type{Name: ti.TypeName, OID: ti.Oid, Codec: &pgtype.ArrayCodec{ElementType: dt}} + type_ = &pgtype.Type{Name: ti.TypeName, OID: ti.Oid, Codec: &pgtype.ArrayCodec{ElementType: dt, Delimiter: parseTypeDelimiter(ti.Typdelim)}} case "c": // composite var fields []pgtype.CompositeCodecField for i, fieldName := range ti.Attnames { diff --git a/vendor/github.com/jackc/pgx/v5/doc.go b/vendor/github.com/jackc/pgx/v5/doc.go index 5e48701..f20b3f6 100644 --- a/vendor/github.com/jackc/pgx/v5/doc.go +++ b/vendor/github.com/jackc/pgx/v5/doc.go @@ -210,8 +210,18 @@ implemented on top of [pgconn.PgConn]. The [Conn.PgConn] method can be used to a PgBouncer -By default pgx automatically uses prepared statements. Prepared statements are incompatible with PgBouncer. This can be -disabled by setting a different [QueryExecMode] in [ConnConfig.DefaultQueryExecMode]. +By default pgx automatically uses protocol-level named prepared statements. PgBouncer 1.21.0 and newer can use these +prepared statements in transaction and statement pooling modes when its max_prepared_statements setting is greater than +zero. In this configuration the default [QueryExecModeCacheStatement] mode may be used. See the PgBouncer documentation +for limitations and configuration details: https://www.pgbouncer.org/config.html#max_prepared_statements. + +When using an older PgBouncer version or when prepared statement support is disabled, set +[ConnConfig.DefaultQueryExecMode] to [QueryExecModeExec]. [QueryExecModeCacheDescribe] and +[QueryExecModeSimpleProtocol] are also compatible with PgBouncer. Prefer [QueryExecModeExec] over +[QueryExecModeSimpleProtocol] whenever possible. Do not use [QueryExecModeDescribeExec] with transaction pooling because +PgBouncer may assign a different server connection between the describe and execute steps. + +SQL-level prepared statements created with PREPARE are not supported by PgBouncer in transaction pooling mode. */ package pgx diff --git a/vendor/github.com/jackc/pgx/v5/internal/iobufpool/iobufpool.go b/vendor/github.com/jackc/pgx/v5/internal/iobufpool/iobufpool.go index abc41f6..4f133b6 100644 --- a/vendor/github.com/jackc/pgx/v5/internal/iobufpool/iobufpool.go +++ b/vendor/github.com/jackc/pgx/v5/internal/iobufpool/iobufpool.go @@ -33,7 +33,7 @@ func Get(size int) *[]byte { return &buf } - ptrBuf := (pools[i].Get().(*[]byte)) + ptrBuf := pools[i].Get().(*[]byte) *ptrBuf = (*ptrBuf)[:size] return ptrBuf diff --git a/vendor/github.com/jackc/pgx/v5/internal/pgdatetime/pgdatetime.go b/vendor/github.com/jackc/pgx/v5/internal/pgdatetime/pgdatetime.go new file mode 100644 index 0000000..306d0c7 --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/internal/pgdatetime/pgdatetime.go @@ -0,0 +1,118 @@ +// Package pgdatetime writes PostgreSQL's ISO date/time text format. +// +// The format is the one EncodeDateTime and AppendSeconds produce in +// src/backend/utils/adt/datetime.c under the default ISO DateStyle: +// +// stamp = date [ SP time [ zone ] ] [ " BC" ] +// date = year "-" 2DIGIT "-" 2DIGIT +// time = 2DIGIT ":" 2DIGIT ":" 2DIGIT [ "." 1*6DIGIT ] +// +// The year is zero-padded to four digits and then grows as wide as it needs, the +// fractional part is omitted when it is zero and never carries trailing zeros, and " BC" +// comes last, after any time zone. Go's layout language can express none of those, which +// is why the format is written out here rather than left to time.Format. +// +// This is the only implementation of the format in pgx. pgtype's date, timestamp and +// timestamptz text encoders and the simple protocol's query sanitizer all go through it, +// so they cannot drift apart. +package pgdatetime + +import ( + "strconv" + "time" +) + +// AppendDate appends t's date. The fields are read from t in its own location. +func AppendDate(buf []byte, t time.Time) []byte { + year, bc := splitBCYear(t.Year()) + buf = appendDate(buf, year, int(t.Month()), t.Day()) + return appendEra(buf, bc) +} + +// AppendTimestamp appends t's date and time of day followed by zone, which goes before +// the era suffix because PostgreSQL writes " BC" last. zone is "Z" for a value already +// converted to UTC and "" for a type that carries no time zone at all. +// +// The fields are read from t in its own location, so a caller that wants the UTC instant +// must convert first. Sub-microsecond precision is discarded, matching the resolution of +// the wire format. +func AppendTimestamp(buf []byte, t time.Time, zone string) []byte { + year, bc := splitBCYear(t.Year()) + buf = appendDate(buf, year, int(t.Month()), t.Day()) + buf = append(buf, ' ') + buf = appendTime(buf, t.Hour(), t.Minute(), t.Second(), t.Nanosecond()) + buf = append(buf, zone...) + return appendEra(buf, bc) +} + +// splitBCYear converts an astronomical year, in which 1 BC is 0 and 2 BC is -1, to the +// year and era PostgreSQL writes. +func splitBCYear(year int) (displayYear int, bc bool) { + if year <= 0 { + return 1 - year, true + } + return year, false +} + +func appendEra(buf []byte, bc bool) []byte { + if bc { + buf = append(buf, " BC"...) + } + return buf +} + +// appendDate appends year-month-day. year is a displayed year from splitBCYear rather +// than an astronomical one. +func appendDate(buf []byte, year, month, day int) []byte { + buf = appendYear(buf, year) + buf = append(buf, '-') + buf = append2Digits(buf, month) + buf = append(buf, '-') + return append2Digits(buf, day) +} + +func appendTime(buf []byte, hour, min, sec, nsec int) []byte { + buf = append2Digits(buf, hour) + buf = append(buf, ':') + buf = append2Digits(buf, min) + buf = append(buf, ':') + buf = append2Digits(buf, sec) + + usec := nsec / 1000 + if usec == 0 { + return buf + } + + var frac [6]byte + for i := 5; i >= 0; i-- { + frac[i] = byte('0' + usec%10) + usec /= 10 + } + + // The server trims trailing zeros, so ".5" rather than ".500000". + end := len(frac) + for end > 1 && frac[end-1] == '0' { + end-- + } + + buf = append(buf, '.') + return append(buf, frac[:end]...) +} + +// append2Digits appends v as exactly two digits. Every field but the year is fixed width +// and comes from a time.Time accessor, so v is always in [0, 99] and the general routine +// below is not needed. +func append2Digits(buf []byte, v int) []byte { + return append(buf, byte('0'+v/10), byte('0'+v%10)) +} + +// appendYear appends v zero-padded to four digits, or wider when the year needs it. v is +// a displayed year from splitBCYear and so is always positive. +func appendYear(buf []byte, v int) []byte { + var tmp [20]byte + s := strconv.AppendInt(tmp[:0], int64(v), 10) + for i := len(s); i < 4; i++ { + buf = append(buf, '0') + } + return append(buf, s...) +} diff --git a/vendor/github.com/jackc/pgx/v5/internal/pgio/doc.go b/vendor/github.com/jackc/pgx/v5/internal/pgio/doc.go index ef2dcc7..60e2fbb 100644 --- a/vendor/github.com/jackc/pgx/v5/internal/pgio/doc.go +++ b/vendor/github.com/jackc/pgx/v5/internal/pgio/doc.go @@ -1,6 +1,8 @@ -// Package pgio is a low-level toolkit building messages in the PostgreSQL wire protocol. +// Package pgio is a low-level toolkit for building and parsing messages in the +// PostgreSQL wire protocol. /* pgio provides functions for appending integers to a []byte while doing byte -order conversion. +order conversion, and a bounds-checked Reader for parsing binary values from +untrusted input. */ package pgio diff --git a/vendor/github.com/jackc/pgx/v5/internal/pgio/read.go b/vendor/github.com/jackc/pgx/v5/internal/pgio/read.go new file mode 100644 index 0000000..9ea42c8 --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/internal/pgio/read.go @@ -0,0 +1,250 @@ +package pgio + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" +) + +// ErrInsufficientBytes is wrapped by all Reader errors caused by a read past +// the end of the source. +var ErrInsufficientBytes = errors.New("insufficient bytes") + +// Reader is a bounds-checked reader for the PostgreSQL binary format. It is +// designed so that decoders of untrusted input cannot forget a length check: +// every read validates against the remaining bytes, and the first failure +// sticks. After a failure all subsequent reads return zero values, so a +// decoder can read an entire structure without intermediate error checks and +// inspect Err or Finish once at the end. +// +// Reads never panic. A decoder that branches on a value it just read (e.g. an +// element count used to size an allocation) should check Err before acting on +// the value. +type Reader struct { + s []byte + rp int + err error +} + +func NewReader(s []byte) *Reader { + return &Reader{s: s} +} + +func (r *Reader) fail(err error) { + if r.err == nil { + r.err = err + } +} + +// need reports whether n more bytes are available, recording an error if not. +// +// The error is built by failShort rather than here so that need stays within +// the inliner's budget. Reads of fixed-size values are the hottest path in the +// driver, and an un-inlined bounds check costs more than the read itself. +func (r *Reader) need(n int) bool { + if r.err != nil { + return false + } + if len(r.s)-r.rp < n { + r.failShort(n) + return false + } + return true +} + +func (r *Reader) failShort(n int) { + r.fail(fmt.Errorf("%w: %d needed at offset %d, %d remain", ErrInsufficientBytes, n, r.rp, len(r.s)-r.rp)) +} + +// Err returns the first error encountered, if any. +func (r *Reader) Err() error { + return r.err +} + +// Remaining returns the number of unread bytes. +func (r *Reader) Remaining() int { + return len(r.s) - r.rp +} + +func (r *Reader) Byte() byte { + if !r.need(1) { + return 0 + } + b := r.s[r.rp] + r.rp += 1 + return b +} + +func (r *Reader) Uint16() uint16 { + if !r.need(2) { + return 0 + } + n := binary.BigEndian.Uint16(r.s[r.rp:]) + r.rp += 2 + return n +} + +func (r *Reader) Int16() int16 { + return int16(r.Uint16()) +} + +func (r *Reader) Uint32() uint32 { + if !r.need(4) { + return 0 + } + n := binary.BigEndian.Uint32(r.s[r.rp:]) + r.rp += 4 + return n +} + +func (r *Reader) Int32() int32 { + return int32(r.Uint32()) +} + +func (r *Reader) Uint64() uint64 { + if !r.need(8) { + return 0 + } + n := binary.BigEndian.Uint64(r.s[r.rp:]) + r.rp += 8 + return n +} + +func (r *Reader) Int64() int64 { + return int64(r.Uint64()) +} + +// Bytes reads the next n bytes. The returned slice aliases the source; it is +// not a copy. +func (r *Reader) Bytes(n int) []byte { + if n < 0 { + r.fail(fmt.Errorf("invalid byte count %d at offset %d", n, r.rp)) + return nil + } + if !r.need(n) { + return nil + } + b := r.s[r.rp : r.rp+n] + r.rp += n + return b +} + +// CString reads a NUL-terminated string, returning the bytes before the +// terminator and consuming the terminator. The returned slice aliases the +// source; it is not a copy. +func (r *Reader) CString() []byte { + if r.err != nil { + return nil + } + i := bytes.IndexByte(r.s[r.rp:], 0) + if i < 0 { + r.fail(fmt.Errorf("%w: unterminated string at offset %d", ErrInsufficientBytes, r.rp)) + return nil + } + b := r.s[r.rp : r.rp+i] + r.rp += i + 1 + return b +} + +// Count reads an int32 element count and validates it against the remaining +// bytes: the count must be non-negative, and since each element occupies at +// least minElemSize bytes, count*minElemSize must not exceed the remaining +// message. This bounds allocations sized from the count against a malicious +// or corrupt message claiming a huge count. Returns 0 on any failure. +func (r *Reader) Count(minElemSize int) int { + offset := r.rp + count := int(r.Int32()) + if r.err != nil { + return 0 + } + if count < 0 { + r.fail(fmt.Errorf("invalid element count %d at offset %d", count, offset)) + return 0 + } + if minElemSize < 1 { + minElemSize = 1 + } + if count > r.Remaining()/minElemSize { + r.fail(fmt.Errorf("element count %d at offset %d exceeds %d remaining bytes", count, offset, r.Remaining())) + return 0 + } + return count +} + +// Value reads an int32 length followed by that many bytes — the standard +// PostgreSQL binary representation of a value. A length of -1 means NULL and +// returns (nil, true). Any other negative length is an error. The returned +// slice aliases the source; null is only meaningful if Err returns nil. +func (r *Reader) Value() (data []byte, null bool) { + offset := r.rp + length := r.Int32() + if r.err != nil { + return nil, false + } + if length == -1 { + return nil, true + } + if length < 0 { + r.fail(fmt.Errorf("invalid value length %d at offset %d", length, offset)) + return nil, false + } + return r.Bytes(int(length)), false +} + +// Finish returns the first error encountered, or an error if unread bytes +// remain. Decoders that must consume the entire source should end with Finish. +func (r *Reader) Finish() error { + if r.err != nil { + return r.err + } + if r.rp != len(r.s) { + return r.errTrailing() + } + return nil +} + +func (r *Reader) errTrailing() error { + return fmt.Errorf("%d unexpected trailing bytes at offset %d", len(r.s)-r.rp, r.rp) +} + +// ErrInvalidLength is wrapped by the errors returned from the exact-length +// read functions below. +var ErrInvalidLength = errors.New("invalid length") + +func errLength(want, got int) error { + return fmt.Errorf("%w: expected %d bytes, got %d", ErrInvalidLength, want, got) +} + +// The Uint*Exact functions read a single fixed-size value that makes up an +// entire message, which is what the scan plans for the fixed-size PostgreSQL +// types receive. They are the counterpart to Reader for values that have no +// internal structure: there is no position to track and no error to make +// sticky, just an exact-length assertion the caller cannot skip. Keeping them +// separate from Reader is deliberate — these are the hottest decode paths in +// the driver and they are small enough for the compiler to inline, which a +// Reader method carrying a bounds check and a read pointer is not. + +// Uint16Exact returns the big-endian uint16 in src, which must be exactly 2 bytes. +func Uint16Exact(src []byte) (uint16, error) { + if len(src) != 2 { + return 0, errLength(2, len(src)) + } + return binary.BigEndian.Uint16(src), nil +} + +// Uint32Exact returns the big-endian uint32 in src, which must be exactly 4 bytes. +func Uint32Exact(src []byte) (uint32, error) { + if len(src) != 4 { + return 0, errLength(4, len(src)) + } + return binary.BigEndian.Uint32(src), nil +} + +// Uint64Exact returns the big-endian uint64 in src, which must be exactly 8 bytes. +func Uint64Exact(src []byte) (uint64, error) { + if len(src) != 8 { + return 0, errLength(8, len(src)) + } + return binary.BigEndian.Uint64(src), nil +} diff --git a/vendor/github.com/jackc/pgx/v5/internal/sanitize/sanitize.go b/vendor/github.com/jackc/pgx/v5/internal/sanitize/sanitize.go index 033a414..2e0230b 100644 --- a/vendor/github.com/jackc/pgx/v5/internal/sanitize/sanitize.go +++ b/vendor/github.com/jackc/pgx/v5/internal/sanitize/sanitize.go @@ -11,6 +11,8 @@ import ( "sync" "time" "unicode/utf8" + + "github.com/jackc/pgx/v5/internal/pgdatetime" ) // Part is either a string or an int. A string is raw SQL. An int is a @@ -81,8 +83,12 @@ func (q *Query) Sanitize(args ...any) (string, error) { case string: p = QuoteString(buf.AvailableBuffer(), arg) case time.Time: - p = arg.Truncate(time.Microsecond). - AppendFormat(buf.AvailableBuffer(), "'2006-01-02 15:04:05.999999999Z07:00:00'") + // pgtype's timestamptz text encoder writes the same format. Going through + // the same code keeps the two from drifting apart, and time.Format cannot + // express PostgreSQL's BC era or its variable width year anyway. + p = append(buf.AvailableBuffer(), '\'') + p = pgdatetime.AppendTimestamp(p, arg.UTC(), "Z") + p = append(p, '\'') default: return "", fmt.Errorf("invalid arg type: %T", arg) } diff --git a/vendor/github.com/jackc/pgx/v5/internal/stmtcache/lru_cache.go b/vendor/github.com/jackc/pgx/v5/internal/stmtcache/lru_cache.go index b677d29..63173b0 100644 --- a/vendor/github.com/jackc/pgx/v5/internal/stmtcache/lru_cache.go +++ b/vendor/github.com/jackc/pgx/v5/internal/stmtcache/lru_cache.go @@ -62,7 +62,7 @@ func (c *LRUCache) Put(sd *pgconn.StatementDescription) { return } - // The statement may have been invalidated but not yet handled. Do not readd it to the cache. + // The statement may have been invalidated but not yet handled. Do not re-add it to the cache. if _, invalidated := c.invalidSet[sd.SQL]; invalidated { return } diff --git a/vendor/github.com/jackc/pgx/v5/mise.toml b/vendor/github.com/jackc/pgx/v5/mise.toml index 78610d4..97078ec 100644 --- a/vendor/github.com/jackc/pgx/v5/mise.toml +++ b/vendor/github.com/jackc/pgx/v5/mise.toml @@ -1,8 +1,173 @@ +# mise is this project's single development entry point: the same `mise run ` works on +# native macOS, on native Linux, and inside the devcontainer. See DEVELOPMENT.md. +# +# The division of labour: +# mise tool versions, this checkout's environment, one-shot tasks +# process-compose long-running services (process-compose.yaml) — five PostgreSQL clusters + CRDB +# port-tamer this checkout's TCP ports (port-tamer.toml -> .dev/ports.env) +# +# PostgreSQL itself is the one prerequisite mise does not provide; DEVELOPMENT.md §1 lists it. + [tools] -go = '1.26.3' +go = '1.27.0' +ruby = '4.0.4' "go:github.com/go-critic/go-critic/cmd/gocritic" = "latest" "go:github.com/gordonklaus/ineffassign" = "latest" "go:github.com/mdempsky/unconvert" = "latest" "go:golang.org/x/tools/cmd/goimports" = "latest" "go:mvdan.cc/gofumpt" = "latest" -ruby = '4.0.4' + +# The linter .golangci.yml configures and CLAUDE.md documents. It was never actually provisioned +# by the devcontainer, so `golangci-lint run ./...` only worked if you had installed it yourself. +golangci-lint = '2.13.2' + +# The process supervisor for the long-running development services. Pinned like every other tool +# so macOS, Linux, and the devcontainer run the same version. +process-compose = '1.122.0' + +# Per-checkout TCP port allocation. The `github:` backend takes the prebuilt release binary, so it +# does not depend on the Go toolchain this repo pins. +"github:jackc/port-tamer" = '0.1.0' + +# The CockroachDB test target. Previously a container image; the version tracks the one CI installs +# in ci/setup_test.bash. +cockroach = '25.4.4' + +# --- Environment --------------------------------------------------------------------------------- +# +# This checkout's own service endpoints. Loading them here is what makes the per-checkout clusters +# the DEFAULT: a bare `psql` and a bare `go test ./...` read PG*/PGX_TEST_* from the environment, +# so pointing those at .dev/ in one place moves every consumer at once. +# +# Two files with two owners, and the order matters — the derived values are read after the +# allocation they come from: +# +# .dev/ports.env port-tamer's state file (port-tamer.toml declares the names). NAME= +# lines only; port-tamer rewrites it canonically and rejects anything else. +# .dev/derived.env PGHOST/PGPORT/PGUSER/PGDATABASE plus the default target's PGX_TEST_* set — +# scripts/devenv.rb, via scripts/lib/test_targets.rb. +# +# Both are generated per checkout and gitignored; `mise run dev:init` creates them. Their absence +# is not an error — mise skips a missing file and PG* falls back to whatever the shell already had. +[env] +_.file = ['.dev/ports.env', '.dev/derived.env'] + +# Homebrew's postgresql@ formulas are KEG-ONLY: nothing is linked into the prefix bin, so +# psql and pg_isready are not on PATH after `brew install`. The scripts that need the SERVER +# binaries resolve them per major explicitly (scripts/lib/pg_bin.rb), but the CLIENT tools are +# invoked by name from the readiness probes, `rake db:psql`, and the database bootstrap. +# +# The newest major's client is used for every server: libpq is backward compatible, so one psql +# talks to all five clusters. Both standard Homebrew prefixes are listed (Apple silicon, then +# Intel). Neither exists on Linux, where a nonexistent PATH entry is simply ignored, so this is +# inert in the devcontainer. +_.path = [ + '/opt/homebrew/opt/postgresql@18/bin', + '/usr/local/opt/postgresql@18/bin', +] + +# process-compose's control API address. The port comes from this checkout's allocation +# (PC_PORT_NUM in .dev/ports.env), so every `process-compose ...` command reaches THIS checkout's +# stack with no flags. +PC_ADDRESS = "127.0.0.1" + +# Kept separate deliberately: `mise run dev` writes the supervisor log, and a client command such +# as `process-compose process list` would otherwise truncate the log of the stack it is inspecting. +PC_SERVER_LOG_FILE = "{{config_root}}/.dev/process-compose.log" +PC_LOG_FILE = "{{config_root}}/.dev/process-compose-client.log" + +# --- Tasks: the project's development interface --------------------------------------------------- +# +# Thin wrappers. The logic lives in scripts/ and the Rakefile; these exist so a workflow is +# reachable without knowing what is underneath, and so the same command works in every environment. + +[tasks."dev:init"] +description = "Bootstrap this checkout: allocate its ports, decode its certificates" +run = "ruby scripts/devenv.rb ensure" + +[tasks.dev] +description = "Start PostgreSQL 18 and this checkout's on-demand database supervisor" +run = "ruby scripts/dev.rb" + +[tasks."dev:all"] +description = "Start PostgreSQL 14-18 and CockroachDB eagerly under process-compose" +run = "ruby scripts/dev.rb all" + +[tasks."dev:down"] +description = "Stop this checkout's development stack" +run = "ruby scripts/dev.rb down" + +[tasks."dev:wait"] +description = "Wait for a detached development stack to become ready" +run = "ruby scripts/dev_ready.rb --wait" + +[tasks."dev:ready"] +description = "Require the development stack to be running" +run = "ruby scripts/dev_ready.rb" +hide = true + +[tasks."dev:ports"] +description = "Print this checkout's port allocation and where each server's data lives" +run = "rake dev:ports:show" + +[tasks."dev:ports:ensure"] +description = "Allocate this checkout's ports if it has none (idempotent)" +run = "rake dev:ports:ensure" + +[tasks."dev:ports:overwrite"] +description = "Move this checkout to a different port group (stop its services first)" +run = "rake dev:ports:overwrite" + +# --- Databases ----------------------------------------------------------------------------------- + +[tasks."db:start"] +description = "Prewarm database targets (default: pg18); e.g. mise run db:start pg16 crdb" +run = "ruby scripts/dev_services.rb start" + +[tasks."db:stop"] +description = "Stop database targets (default: pg18); e.g. mise run db:stop pg16 crdb" +run = "ruby scripts/dev_services.rb stop" + +# These call scripts/devdb.rb rather than the equivalent rake tasks. mise APPENDS a task's +# arguments to its `run` line, and `rake db:reset 16` does not mean "db:reset with the argument +# 16" — rake reads the 16 as a SECOND TASK, runs db:reset with no major (which is every cluster), +# and only then fails on the unknown task. devdb.rb takes the major as an ordinary argument, so +# `mise run db:reset 16` reaches it as written. `rake db:reset[16]` is still the rake spelling. + +[tasks."db:init"] +description = "Create this checkout's PostgreSQL clusters under .dev (idempotent); e.g. mise run db:init 16" +run = "ruby scripts/devdb.rb init" + +[tasks."db:psql"] +description = "psql against one of this checkout's clusters (default: the newest major)" +run = "ruby scripts/devdb.rb psql" + +[tasks."db:setup"] +description = "Create pgx_test and its roles in a running cluster (idempotent)" +run = "ruby scripts/devdb.rb setup" + +[tasks."db:reset"] +description = "Destroy and recreate this checkout's clusters (destructive; CONFIRM=yes, stack down)" +run = "ruby scripts/devdb.rb reset" + +# --- Test, format, generate ------------------------------------------------------------------------ + +[tasks.test] +description = "Run the test suite against one target (default pg18); e.g. mise run test pg16" +run = "./test.sh" + +[tasks."test:all"] +description = "Run the test suite against every target: PostgreSQL 14-18 and CockroachDB" +run = "./test.sh all" + +[tasks.fmt] +description = "Format all Go sources with goimports" +run = "goimports -w ." + +[tasks.lint] +description = "Run the linters configured in .golangci.yml" +run = "golangci-lint run ./..." + +[tasks.generate] +description = "Regenerate the ERB-templated Go sources" +run = "rake generate" diff --git a/vendor/github.com/jackc/pgx/v5/pgconn/config.go b/vendor/github.com/jackc/pgx/v5/pgconn/config.go index eec7de6..b521a69 100644 --- a/vendor/github.com/jackc/pgx/v5/pgconn/config.go +++ b/vendor/github.com/jackc/pgx/v5/pgconn/config.go @@ -10,7 +10,6 @@ import ( "maps" "math" "net" - "net/url" "os" "path/filepath" "strconv" @@ -43,6 +42,11 @@ type Config struct { LookupFunc LookupFunc // e.g. net.Resolver.LookupHost BuildFrontend BuildFrontendFunc + // MaxProtocolMessageBodyLen is the maximum length of a PostgreSQL wire protocol message body in octets. If a + // message body exceeds this length, reading the message will fail with pgproto3.ExceededMaxBodyLenErr. The default + // value is 0, which means no maximum is enforced. + MaxProtocolMessageBodyLen int + // BuildContextWatcherHandler is called to create a ContextWatcherHandler for a connection. The handler is called // when a context passed to a PgConn method is canceled. BuildContextWatcherHandler func(*PgConn) ctxwatch.Handler @@ -109,6 +113,11 @@ type Config struct { createdByParseConfig bool // Used to enforce created by ParseConfig rule. } +// defaultPort is the port used when neither the connection string, the environment, nor the +// service file supplies one. It is also the per-element fallback for empty entries in a +// multi-host port list. +const defaultPort = "5432" + // connStringKeyAliases maps libpq parameter keywords to the canonical key names this package // uses internally in the parsed-settings map. Most keywords are already canonical; this map // holds only those whose pgx-internal name differs from the libpq spelling. @@ -137,7 +146,11 @@ type ParseConfigOptions struct { // defaults are not checked: only keys that originate from the connString argument. // // Keys may be given in either their libpq spelling ("dbname") or pgx-internal spelling - // ("database"); both are accepted. + // ("database"); both are accepted. The URI-only ssl=true alias for sslmode=require is + // accepted when either "ssl" or "sslmode" is allowed; an explicit sslmode key or a + // non-"true" ssl value only matches its own spelling. Every ssl/sslmode occurrence in + // the connection string is validated, including occurrences superseded by a later + // repeated parameter. // // A nil slice (the default) applies no restriction and matches libpq behaviour. An empty // non-nil slice rejects every key, i.e. connString must be empty. @@ -232,10 +245,10 @@ func NetworkAddress(host string, port uint16) (network, address string) { // to only read from the environment. If a password is not supplied it will attempt to read the .pgpass file. // // # Example Keyword/Value -// user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-ca +// user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-full // // # Example URL -// postgres://jack:secret@pg.example.com:5432/mydb?sslmode=verify-ca +// postgres://jack:secret@pg.example.com:5432/mydb?sslmode=verify-full // // The returned *Config may be modified. However, it is strongly recommended that any configuration that can be done // through the connection string be done there. In particular the fields Host, Port, TLSConfig, and Fallbacks can be @@ -307,6 +320,21 @@ func NetworkAddress(host string, port uint16) (network, address string) { // When multiple hosts are specified, libpq allows them to have different passwords set via the .pgpass file. pgconn // does not. // +// URL query parameters that libpq does not recognize cause libpq to fail with an "invalid URI query parameter" error. +// ParseConfig accepts them: they become runtime parameters or pgx-specific options (e.g. pool_max_conns). +// +// Connection strings containing a NUL byte are rejected, in both URI and keyword/value form. libpq cannot encounter +// one because its conninfo strings are NUL-terminated C strings, but a Go string can carry a NUL into the startup +// packet, where it delimits parameters rather than being data. Settings reaching Config by other routes (a service +// file, or direct assignment to Config.RuntimeParams, User, or Database) are not checked here; a NUL in those is +// caught when the startup message is encoded, and Connect fails rather than sending it. +// +// Error messages from ParseConfig avoid quoting the unredacted connection string and attempt to redact recognizable +// password fields, while libpq quotes the failing input verbatim. This redaction is best effort. An invalid connection +// string can be structurally ambiguous, so pgconn cannot guarantee that every password in malformed input will be +// identified or redacted. Applications should not assume that parse errors are safe to expose when connection strings +// may contain secrets. +// // In addition, ParseConfig accepts the following options: // // - servicefile. @@ -325,11 +353,12 @@ func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Con envSettings := parseEnvSettings() connStringSettings := make(map[string]string) + var urlMeta parseURLMeta if connString != "" { var err error // connString may be a database URL or in PostgreSQL keyword/value format if strings.HasPrefix(connString, "postgres://") || strings.HasPrefix(connString, "postgresql://") { - connStringSettings, err = parseURLSettings(connString) + connStringSettings, urlMeta, err = parseURLSettings(connString) if err != nil { return nil, &ParseConfigError{ConnString: connString, msg: "failed to parse as URL", err: err} } @@ -346,14 +375,77 @@ func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Con for _, k := range options.ConnStringAllowedKeys { allowed[canonicalConnStringKey(k)] = struct{}{} } + notAllowed := func(k string) error { + return &ParseConfigError{ConnString: connString, msg: fmt.Sprintf("connection string key %q is not in ConnStringAllowedKeys", k)} + } + _, sslAllowed := allowed["ssl"] + _, sslmodeAllowed := allowed["sslmode"] + + // Repeated-key handling and the URI ssl=true alias rewrite can remove + // ssl/sslmode occurrences from the final settings map, so those two keys + // are validated from what the user actually wrote -- every occurrence, + // fail closed -- rather than from what survived. The alias itself is + // accepted under either spelling, the same way dbname/database are + // interchangeable; an explicit sslmode key or a non-"true" ssl value + // only matches its own spelling. + if urlMeta.sawRawSSLKey && !sslAllowed { + return nil, notAllowed("ssl") + } + if urlMeta.sawExplicitSSLModeKey && !sslmodeAllowed { + return nil, notAllowed("sslmode") + } + if urlMeta.sawSSLTrueAlias && !sslAllowed && !sslmodeAllowed { + return nil, notAllowed("ssl") + } + for k := range connStringSettings { - if _, ok := allowed[k]; !ok { - return nil, &ParseConfigError{ConnString: connString, msg: fmt.Sprintf("connection string key %q is not in ConnStringAllowedKeys", k)} + if _, ok := allowed[k]; ok { + continue + } + // A multi-host URI with no explicit ports produces an implied + // all-empty port list (e.g. ","), matching libpq. Only that list + // -- identified by the parser from the raw syntax, not inferred + // from the value -- is exempt from the allow-list: it carries no + // user-supplied port text. An explicit empty port (?port= in a + // URI or port= in keyword/value form) is user-supplied, and + // because a present-but-empty port still shadows PGPORT it must + // pass the allow-list like any other key. + if k == "port" && urlMeta.impliedEmptyPortList { + continue + } + // A surviving ssl or sslmode entry from a URI was already + // validated above against every spelling the user wrote. + if k == "ssl" && urlMeta.sawRawSSLKey { + continue + } + if k == "sslmode" && (urlMeta.sawSSLTrueAlias || urlMeta.sawExplicitSSLModeKey) { + continue } + return nil, notAllowed(k) } } settings := mergeSettings(defaultSettings, envSettings, connStringSettings) + + // The home-directory-derived defaults (passfile, servicefile, sslcert, + // sslkey, sslrootcert) are already present in settings at this point: + // defaultSettings resolves them via the user's home directory, which is + // safe and cheap to look up (see defaults.go / defaults_windows.go). + // + // The default PostgreSQL user name is different: resolving it requires + // looking up the OS user account, which can be slow or, in some + // restricted container environments, crash the process. So that lookup + // is memoized and only performed lazily below, the first time it is + // actually needed -- i.e. only when a connection string or environment + // does not already supply a user. + var cachedOSUserSettings map[string]string + lazyOSUserSettings := func() map[string]string { + if cachedOSUserSettings == nil { + cachedOSUserSettings = osUserSettings() + } + return cachedOSUserSettings + } + if service, present := settings["service"]; present { serviceSettings, err := parseServiceSettings(settings["servicefile"], service) if err != nil { @@ -363,6 +455,13 @@ func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Con settings = mergeSettings(defaultSettings, envSettings, serviceSettings, connStringSettings) } + // Only fall back to the OS user account for the default PostgreSQL user + // name when it was not already supplied by the connection string, + // environment, or service file. + if settings["user"] == "" { + settings = mergeSettings(lazyOSUserSettings(), settings) + } + config := &Config{ createdByParseConfig: true, Database: settings["database"], @@ -442,17 +541,41 @@ func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Con hosts := strings.Split(settings["host"], ",") ports := strings.Split(settings["port"], ",") + // Like libpq, if exactly one port is given it applies to all hosts; + // otherwise there must be exactly one port per host. Empty list elements + // mean "use the default". + if len(ports) > 1 && len(ports) != len(hosts) { + return nil, &ParseConfigError{ConnString: connString, msg: fmt.Sprintf("could not match %d port numbers to %d hosts", len(ports), len(hosts))} + } + + // defaultHost stats candidate socket directories, so resolve it at most + // once even when several host list elements are empty. It never returns "". + resolvedDefaultHost := "" for i, host := range hosts { - var portStr string - if i < len(ports) { + if host == "" { + if resolvedDefaultHost == "" { + resolvedDefaultHost = defaultHost() + } + host = resolvedDefaultHost + } + + portStr := ports[0] + if len(ports) > 1 { portStr = ports[i] - } else { - portStr = ports[0] + } + if portStr == "" { + portStr = defaultPort } + // The strconv error is deliberately not wrapped: it quotes the + // offending text, and in a malformed URI the bytes that land in the + // port position can be a mislaid password (postgres://u:sec:ret@h + // parses "sec:ret@h" as the port). The best-effort-redacted connection + // string in ParseConfigError provides the available context without + // deliberately quoting the offending port text again. port, err := parsePort(portStr) if err != nil { - return nil, &ParseConfigError{ConnString: connString, msg: "invalid port", err: err} + return nil, &ParseConfigError{ConnString: connString, msg: "invalid port"} } var tlsConfigs []*tls.Config @@ -483,14 +606,13 @@ func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Con config.Fallbacks = fallbacks[1:] config.SSLNegotiation = settings["sslnegotiation"] - passfile, err := pgpassfile.ReadPassfile(settings["passfile"]) - if err == nil { - if config.Password == "" { + if config.Password == "" { + passfile, err := pgpassfile.ReadPassfile(settings["passfile"]) + if err == nil { host := config.Host if network, _ := NetworkAddress(config.Host, config.Port); network == "unix" { host = "localhost" } - config.Password = passfile.FindPassword(host, strconv.Itoa(int(config.Port)), config.Database, config.User) } } @@ -616,76 +738,46 @@ func parseEnvSettings() map[string]string { return settings } -func parseURLSettings(connString string) (map[string]string, error) { - settings := make(map[string]string) - - parsedURL, err := url.Parse(connString) - if err != nil { - if urlErr := new(url.Error); errors.As(err, &urlErr) { - return nil, urlErr.Err - } - return nil, err - } +var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} - if parsedURL.User != nil { - if u := parsedURL.User.Username(); u != "" { - settings["user"] = u - } - if password, present := parsedURL.User.Password(); present { - settings["password"] = password - } +// unescapeKeywordValue applies libpq's backslash rule to the raw text of a +// keyword/value value: a backslash is dropped and whatever follows it is taken +// literally, whatever that character is. A backslash at the very end escapes +// the end of the string, so it is dropped and contributes nothing -- which is +// how libpq accepts `host=a\`. Only the unquoted branch can reach that case; +// inside quotes the escaped terminator leaves the string unterminated and the +// caller has already rejected it. +func unescapeKeywordValue(s string) string { + if !strings.ContainsRune(s, '\\') { + return s } - // Handle multiple host:port's in url.Host by splitting them into host,host,host and port,port,port. - var hosts []string - var ports []string - for host := range strings.SplitSeq(parsedURL.Host, ",") { - if host == "" { - continue - } - if isIPOnly(host) { - hosts = append(hosts, strings.Trim(host, "[]")) - continue - } - h, p, err := net.SplitHostPort(host) - if err != nil { - return nil, fmt.Errorf("failed to split host:port in '%s', err: %w", host, err) - } - if h != "" { - hosts = append(hosts, h) - } - if p != "" { - ports = append(ports, p) + var sb strings.Builder + sb.Grow(len(s)) + for i := 0; i < len(s); i++ { + if s[i] == '\\' { + i++ + if i == len(s) { + break + } } + sb.WriteByte(s[i]) } - if len(hosts) > 0 { - settings["host"] = strings.Join(hosts, ",") - } - if len(ports) > 0 { - settings["port"] = strings.Join(ports, ",") - } - - database := strings.TrimLeft(parsedURL.Path, "/") - if database != "" { - settings["database"] = database - } - - for k, v := range parsedURL.Query() { - settings[canonicalConnStringKey(k)] = v[0] - } - - return settings, nil -} - -func isIPOnly(host string) bool { - return net.ParseIP(strings.Trim(host, "[]")) != nil || !strings.Contains(host, ":") + return sb.String() } -var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} - func parseKeywordValueSettings(s string) (map[string]string, error) { settings := make(map[string]string) + // Reject NUL bytes up front, as parseURLSettings does. libpq never sees one + // because its conninfo strings are NUL-terminated C strings; a Go string can + // carry a NUL through to the startup packet, where it acts as a parameter + // delimiter rather than data. StartupMessage.Encode refuses such parameters, + // but failing here reports the problem against the input that caused it. + if strings.IndexByte(s, 0) >= 0 { + return nil, errors.New("forbidden NUL byte in connection string") + } + // Trim any leading whitespace so that the loop exits cleanly when only // spaces remain (e.g. trailing spaces after the last value). s = strings.TrimLeft(s, " \t\n\r\v\f") @@ -697,6 +789,15 @@ func parseKeywordValueSettings(s string) (map[string]string, error) { } key = strings.Trim(s[:eqIdx], " \t\n\r\v\f") + // libpq reads a keyword as a run of non-space characters and then + // requires the next non-space character to be '=', so whitespace + // inside a keyword terminates it and is an error. Trimming alone would + // accept the space as part of the key and turn a typo into a bogus + // RuntimeParam that only the server rejects. Report it as libpq does, + // naming the keyword it had read. + if i := strings.IndexAny(key, " \t\n\r\v\f"); i >= 0 { + return nil, fmt.Errorf(`missing "=" after %q in connection info string`, key[:i]) + } s = strings.TrimLeft(s[eqIdx+1:], " \t\n\r\v\f") switch { case len(s) == 0: @@ -708,12 +809,15 @@ func parseKeywordValueSettings(s string) (map[string]string, error) { } if s[end] == '\\' { end++ + // A trailing backslash escapes the end of the string. + // libpq drops it and ends the value there. Break rather + // than let the loop's post-increment push end past len(s). if end == len(s) { - return nil, errors.New("invalid backslash") + break } } } - val = strings.ReplaceAll(strings.ReplaceAll(s[:end], "\\\\", "\\"), "\\'", "'") + val = unescapeKeywordValue(s[:end]) // Consume the value and trim any subsequent whitespace so that // multiple trailing spaces don't cause a spurious parse failure. s = strings.TrimLeft(s[end:], " \t\n\r\v\f") @@ -726,12 +830,15 @@ func parseKeywordValueSettings(s string) (map[string]string, error) { } if s[end] == '\\' { end++ + if end == len(s) { + return nil, errors.New("unterminated quoted string in connection info string") + } } } if end == len(s) { return nil, errors.New("unterminated quoted string in connection info string") } - val = strings.ReplaceAll(strings.ReplaceAll(s[:end], "\\\\", "\\"), "\\'", "'") + val = unescapeKeywordValue(s[:end]) // Consume the closing quote and any subsequent whitespace. s = strings.TrimLeft(s[end+1:], " \t\n\r\v\f") } @@ -1001,6 +1108,17 @@ func makeConnectTimeoutDialFunc(timeout time.Duration) DialFunc { return d.DialContext } +var ( + // ErrReadOnlyConnection is returned when a read-write connection is required but the connection is read-only. + ErrReadOnlyConnection = errors.New("read only connection") + // ErrReadWriteConnection is returned when a read-only connection is required but the connection is read-write. + ErrReadWriteConnection = errors.New("connection is not read only") + // ErrPrimaryConnection is returned when a standby connection is required but the server is primary. + ErrPrimaryConnection = errors.New("server is not in hot standby mode") + // ErrStandbyConnection is returned when a primary connection is required but the server is in standby mode. + ErrStandbyConnection = errors.New("server is in standby mode") +) + // ValidateConnectTargetSessionAttrsReadWrite is a ValidateConnectFunc that implements libpq compatible // target_session_attrs=read-write. func ValidateConnectTargetSessionAttrsReadWrite(ctx context.Context, pgConn *PgConn) error { @@ -1010,7 +1128,7 @@ func ValidateConnectTargetSessionAttrsReadWrite(ctx context.Context, pgConn *PgC } if string(result[0].Rows[0][0]) == "on" { - return errors.New("read only connection") + return ErrReadOnlyConnection } return nil @@ -1025,7 +1143,7 @@ func ValidateConnectTargetSessionAttrsReadOnly(ctx context.Context, pgConn *PgCo } if string(result[0].Rows[0][0]) != "on" { - return errors.New("connection is not read only") + return ErrReadWriteConnection } return nil @@ -1040,7 +1158,7 @@ func ValidateConnectTargetSessionAttrsStandby(ctx context.Context, pgConn *PgCon } if string(result[0].Rows[0][0]) != "t" { - return errors.New("server is not in hot standby mode") + return ErrPrimaryConnection } return nil @@ -1055,7 +1173,7 @@ func ValidateConnectTargetSessionAttrsPrimary(ctx context.Context, pgConn *PgCon } if string(result[0].Rows[0][0]) == "t" { - return errors.New("server is in standby mode") + return ErrStandbyConnection } return nil @@ -1070,7 +1188,7 @@ func ValidateConnectTargetSessionAttrsPreferStandby(ctx context.Context, pgConn } if string(result[0].Rows[0][0]) != "t" { - return &NotPreferredError{err: errors.New("server is not in hot standby mode")} + return &NotPreferredError{err: ErrPrimaryConnection} } return nil diff --git a/vendor/github.com/jackc/pgx/v5/pgconn/defaults.go b/vendor/github.com/jackc/pgx/v5/pgconn/defaults.go index 1dd514f..03b0d33 100644 --- a/vendor/github.com/jackc/pgx/v5/pgconn/defaults.go +++ b/vendor/github.com/jackc/pgx/v5/pgconn/defaults.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package pgconn @@ -9,22 +8,46 @@ import ( "path/filepath" ) +// currentOSUser resolves the current OS user account. It is a seam so tests +// can simulate os/user.Current failing -- including the unrecoverable +// crashes reported in some restricted/broken-NSS container environments -- +// without needing such an environment. +var currentOSUser = user.Current + +// userHomeDir resolves the current user's home directory. It is a seam so +// tests can simulate os.UserHomeDir failing without needing to unset $HOME +// for the whole process. +// +// os.UserHomeDir only reads the $HOME environment variable on this platform; +// it never calls into cgo or NSS, so unlike os/user.Current it cannot crash +// in restricted/broken container environments. That also means it can +// resolve a different directory than the OS account's passwd-file home +// directory if $HOME has been overridden -- this is an accepted tradeoff so +// that home-directory-derived defaults (pgpass, pg_service.conf, client SSL +// certificate/key/root files) keep resolving even when the OS user account +// lookup itself is unavailable or unsafe to call. +var userHomeDir = os.UserHomeDir + func defaultSettings() map[string]string { settings := make(map[string]string) settings["host"] = defaultHost() - settings["port"] = "5432" + settings["port"] = defaultPort + settings["target_session_attrs"] = "any" - // Default to the OS user name. Purposely ignoring err getting user name from - // OS. The client application will simply have to specify the user in that - // case (which they typically will be doing anyway). - user, err := user.Current() - if err == nil { - settings["user"] = user.Username - settings["passfile"] = filepath.Join(user.HomeDir, ".pgpass") - settings["servicefile"] = filepath.Join(user.HomeDir, ".pg_service.conf") - sslcert := filepath.Join(user.HomeDir, ".postgresql", "postgresql.crt") - sslkey := filepath.Join(user.HomeDir, ".postgresql", "postgresql.key") + // The home-directory-derived defaults (~/.pgpass, ~/.pg_service.conf, + // and the client SSL certificate/key/root files under ~/.postgresql) + // only need the user's home directory, not the full OS user account, so + // they are resolved unconditionally here via os.UserHomeDir. This keeps + // them available even when looking up the OS user account (needed only + // for the default PostgreSQL user name, see osUserSettings) is slow, + // fails, or -- in some restricted container environments with a broken + // NSS/CGO setup -- would crash the process. + if homeDir, err := userHomeDir(); err == nil { + settings["passfile"] = filepath.Join(homeDir, ".pgpass") + settings["servicefile"] = filepath.Join(homeDir, ".pg_service.conf") + sslcert := filepath.Join(homeDir, ".postgresql", "postgresql.crt") + sslkey := filepath.Join(homeDir, ".postgresql", "postgresql.key") if _, err := os.Stat(sslcert); err == nil { if _, err := os.Stat(sslkey); err == nil { // Both the cert and key must be present to use them, or do not use either @@ -32,13 +55,33 @@ func defaultSettings() map[string]string { settings["sslkey"] = sslkey } } - sslrootcert := filepath.Join(user.HomeDir, ".postgresql", "root.crt") + sslrootcert := filepath.Join(homeDir, ".postgresql", "root.crt") if _, err := os.Stat(sslrootcert); err == nil { settings["sslrootcert"] = sslrootcert } } - settings["target_session_attrs"] = "any" + return settings +} + +// osUserSettings returns the default PostgreSQL user name derived from the +// current OS user account. +// +// Resolving this requires looking up the OS user account, which can be slow +// or, in some restricted container environments (e.g. distroless images +// with a broken NSS/CGO setup), crash the process. Callers must only call +// osUserSettings when the default user name is not already supplied by the +// connection string, environment, or service file. +func osUserSettings() map[string]string { + settings := make(map[string]string) + + // Default to the OS user name. Purposely ignoring err getting user name from + // OS. The client application will simply have to specify the user in that + // case (which they typically will be doing anyway). + user, err := currentOSUser() + if err == nil { + settings["user"] = user.Username + } return settings } diff --git a/vendor/github.com/jackc/pgx/v5/pgconn/defaults_windows.go b/vendor/github.com/jackc/pgx/v5/pgconn/defaults_windows.go index 33b4a1f..f59e251 100644 --- a/vendor/github.com/jackc/pgx/v5/pgconn/defaults_windows.go +++ b/vendor/github.com/jackc/pgx/v5/pgconn/defaults_windows.go @@ -7,28 +7,44 @@ import ( "strings" ) +// currentOSUser resolves the current OS user account. It is a seam so tests +// can simulate os/user.Current failing -- including the unrecoverable +// crashes reported in some restricted/broken-NSS container environments -- +// without needing such an environment. +var currentOSUser = user.Current + +// userHomeDir resolves the current user's home directory. It is a seam so +// tests can simulate os.UserHomeDir failing without needing to unset the +// relevant environment variables for the whole process. +// +// os.UserHomeDir only reads %USERPROFILE% (falling back to +// %HOMEDRIVE%+%HOMEPATH%) on this platform; it never calls into the Windows +// user account APIs, so unlike os/user.Current it cannot crash in +// restricted/broken container environments. That also means it can resolve +// a different directory than the OS account API's reported home directory +// if those environment variables have been overridden -- this is an +// accepted tradeoff so that home-directory-derived defaults (pg_service.conf) +// keep resolving even when the OS user account lookup itself is unavailable +// or unsafe to call. +var userHomeDir = os.UserHomeDir + func defaultSettings() map[string]string { settings := make(map[string]string) settings["host"] = defaultHost() - settings["port"] = "5432" - - // Default to the OS user name. Purposely ignoring err getting user name from - // OS. The client application will simply have to specify the user in that - // case (which they typically will be doing anyway). - user, err := user.Current() - appData := os.Getenv("APPDATA") - if err == nil { - // Windows gives us the username here as `DOMAIN\user` or `LOCALPCNAME\user`, - // but the libpq default is just the `user` portion, so we strip off the first part. - username := user.Username - if strings.Contains(username, "\\") { - username = username[strings.LastIndex(username, "\\")+1:] - } + settings["port"] = defaultPort + settings["target_session_attrs"] = "any" - settings["user"] = username + // The %APPDATA%\postgresql-derived defaults (pgpass.conf and the client + // SSL certificate/key/root files) only need the APPDATA environment + // variable, not the full OS user account, so they are resolved + // unconditionally here. This keeps them available even when looking up + // the OS user account (needed only for the default PostgreSQL user + // name, see osUserSettings) is slow, fails, or -- in some restricted + // container environments with a broken NSS/CGO setup -- would crash the + // process. + if appData := os.Getenv("APPDATA"); appData != "" { settings["passfile"] = filepath.Join(appData, "postgresql", "pgpass.conf") - settings["servicefile"] = filepath.Join(user.HomeDir, ".pg_service.conf") sslcert := filepath.Join(appData, "postgresql", "postgresql.crt") sslkey := filepath.Join(appData, "postgresql", "postgresql.key") if _, err := os.Stat(sslcert); err == nil { @@ -44,7 +60,41 @@ func defaultSettings() map[string]string { } } - settings["target_session_attrs"] = "any" + // The default ~/.pg_service.conf location is derived from the user's + // home directory, resolved the same crash-safe way as on other + // platforms (see defaults.go). + if homeDir, err := userHomeDir(); err == nil { + settings["servicefile"] = filepath.Join(homeDir, ".pg_service.conf") + } + + return settings +} + +// osUserSettings returns the default PostgreSQL user name derived from the +// current OS user account. +// +// Resolving this requires looking up the OS user account, which can be slow +// or, in some restricted container environments (e.g. distroless images +// with a broken NSS/CGO setup), crash the process. Callers must only call +// osUserSettings when the default user name is not already supplied by the +// connection string, environment, or service file. +func osUserSettings() map[string]string { + settings := make(map[string]string) + + // Default to the OS user name. Purposely ignoring err getting user name from + // OS. The client application will simply have to specify the user in that + // case (which they typically will be doing anyway). + user, err := currentOSUser() + if err == nil { + // Windows gives us the username here as `DOMAIN\user` or `LOCALPCNAME\user`, + // but the libpq default is just the `user` portion, so we strip off the first part. + username := user.Username + if strings.Contains(username, "\\") { + username = username[strings.LastIndex(username, "\\")+1:] + } + + settings["user"] = username + } return settings } diff --git a/vendor/github.com/jackc/pgx/v5/pgconn/errors.go b/vendor/github.com/jackc/pgx/v5/pgconn/errors.go index 9fbe68c..5f38fdd 100644 --- a/vendor/github.com/jackc/pgx/v5/pgconn/errors.go +++ b/vendor/github.com/jackc/pgx/v5/pgconn/errors.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net" - "net/url" "regexp" "strings" ) @@ -118,9 +117,11 @@ func (e *connLockError) Unwrap() error { return nil } -// ParseConfigError is the error returned when a connection string cannot be parsed. +// ParseConfigError is the error returned when a connection string cannot be +// parsed. Its error text masks recognizable passwords on a best-effort basis, +// but malformed input can be too ambiguous to redact completely. type ParseConfigError struct { - ConnString string // The connection string that could not be parsed. + ConnString string // The original, unredacted connection string that could not be parsed. msg string err error } @@ -134,9 +135,11 @@ func NewParseConfigError(conn, msg string, err error) error { } func (e *ParseConfigError) Error() string { - // Now that ParseConfigError is public and ConnString is available to the developer, perhaps it would be better only - // return a static string. That would ensure that the error message cannot leak a password. The ConnString field would - // allow access to the original string if desired and Unwrap would allow access to the underlying error. + // redactPW is necessarily best effort: an invalid connection string can be + // too ambiguous to identify every password. Returning only a static string + // would be the way to guarantee that Error cannot leak one. The public + // ConnString field would still allow access to the original string if + // desired, and Unwrap would allow access to the underlying error. connString := redactPW(e.ConnString) if e.err == nil { return fmt.Sprintf("cannot parse `%s`: %s", connString, e.msg) @@ -227,11 +230,12 @@ func newContextAlreadyDoneError(ctx context.Context) (err error) { return &errTimeout{&contextAlreadyDoneError{err: ctx.Err()}} } +// redactPW masks recognizable password fields on a best-effort basis. It +// cannot guarantee redaction when malformed input makes component boundaries +// ambiguous. func redactPW(connString string) string { if strings.HasPrefix(connString, "postgres://") || strings.HasPrefix(connString, "postgresql://") { - if u, err := url.Parse(connString); err == nil { - return redactURL(u) - } + return redactURLPassword(connString) } quotedKV := regexp.MustCompile(`password='[^']*'`) connString = quotedKV.ReplaceAllLiteralString(connString, "password=xxxxx") @@ -242,14 +246,164 @@ func redactPW(connString string) string { return connString } -func redactURL(u *url.URL) string { - if u == nil { - return "" +// redactURLPassword masks recognizable password values in a connection URI. +// For a URI that parses cleanly the component boundaries are certain, and +// every password position -- the userinfo password and password/sslpassword +// query values -- is reliably masked with the rest of the string left intact. +// It is also deliberately usable without a successful parse -- the strings +// that reach ParseConfigError are often exactly the ones that failed to parse +// -- but malformed syntax can make component boundaries ambiguous, so for +// such input redaction is best-effort and may over-mask. +// +// For URI shapes it recognizes, it mirrors parseURLSettings structurally +// rather than pattern-matching the raw text: the userinfo password is whatever +// follows the first ':' before the terminating '@' (found with the parser's +// lookahead), and a query value is masked when its percent-decoded key +// canonicalizes to password or sslpassword. This uses the same decoding as the +// parser, so encoded spellings like pass%77ord are caught and the whole +// recognized raw value is masked no matter what bytes it contains. +func redactURLPassword(connString string) string { + const mask = "xxxxx" + var b strings.Builder + b.Grow(len(connString)) + + p := connString + for _, prefix := range []string{"postgresql://", "postgres://"} { + if rest, ok := strings.CutPrefix(connString, prefix); ok { + b.WriteString(prefix) + p = rest + break + } + } + + // Userinfo: same lookahead as parseURLSettings. + if i := strings.IndexAny(p, "@/"); i >= 0 && p[i] == '@' { + user, _, hasPassword := strings.Cut(p[:i], ":") + p = p[i+1:] + b.WriteString(user) + if hasPassword { + b.WriteString(":" + mask) + } + b.WriteByte('@') + } + + // Nothing after the userinfo is a password position, but a password can + // land there in malformed URIs: an unencoded '/' or '?' in a userinfo + // password turns everything after it into path or query, stranding the + // '@' (postgres://user:pass/word?x=y@host). As a best-effort heuristic, + // while an '@' remains, mask everything shaped like + // ":candidate-credential@" across the whole + // remainder -- before the query is split off, or a stranded '@' inside + // the query would hide the pattern, and greedily up to each '@', so a + // ':' inside the stranded password (user:sec:ret@host) cannot split the + // mask and leak the part before it. + // + // The heuristic must not run when the connection string is structurally + // unambiguous: then nothing can be stranded, a remaining '@' is ordinary + // data (typically in a query value), and the greedy mask would swallow + // the '/' and '?' delimiters -- hiding the query from the password-key + // masking below, so a password=... query parameter would leak. Valid + // URIs must always redact exactly; the heuristic is reserved for + // malformed input, where only best effort is possible. + if strings.IndexByte(p, '@') >= 0 && !uriStructureUnambiguous(connString) { + brokenUserinfo := regexp.MustCompile(`:[^@]+@`) + p = brokenUserinfo.ReplaceAllLiteralString(p, ":xxxxxx@") + } + + qi := uriQueryStart(p) + if qi < 0 { + b.WriteString(p) + return b.String() + } + b.WriteString(p[:qi]) + query := p[qi+1:] + b.WriteByte('?') + + for i, pair := range strings.Split(query, "&") { + if i > 0 { + b.WriteByte('&') + } + rawKey, _, hasValue := strings.Cut(pair, "=") + if !hasValue { + b.WriteString(pair) + continue + } + switch canonicalConnStringKey(uriDecodeLenient(rawKey)) { + case "password", "sslpassword": + b.WriteString(rawKey) + b.WriteByte('=') + b.WriteString(mask) + default: + b.WriteString(pair) + } + } + return b.String() +} + +// uriStructureUnambiguous reports whether connString parses as a connection +// URI whose component boundaries are certain, meaning redactURLPassword's +// structural walk is exact and no password bytes can sit outside the two +// positions it masks (the userinfo password and password-keyed query values). +// That requires parseURLSettings to accept the string and every port element +// to be numeric or empty: the parser accepts arbitrary text in the port slot +// (postgres://a@u:sec:ret@h parses with port "sec:ret@h"), so a non-numeric +// port may be a mislaid password and keeps the string in best-effort +// territory. +func uriStructureUnambiguous(connString string) bool { + settings, _, err := parseURLSettings(connString) + if err != nil { + return false + } + for part := range strings.SplitSeq(settings["port"], ",") { + for i := 0; i < len(part); i++ { + if part[i] < '0' || part[i] > '9' { + return false + } + } + } + return true +} + +// uriQueryStart returns the index of the '?' that begins the query component +// of p (the post-scheme, post-userinfo part of a connection URI), or -1 if +// there is none. It follows parseURLSettings' structure: '[' at the start of +// a netloc element opens an IPv6 bracket whose contents -- deliberately +// unvalidated -- may contain a literal '?' that is host data, not the query +// delimiter. On an unterminated bracket (the parser errors there) it falls +// back to the first '?' anywhere. With no valid structure to follow, +// over-including text gives the best-effort redactor more candidate pairs to +// inspect. +func uriQueryStart(p string) int { + i := 0 + for { + if i < len(p) && p[i] == '[' { + end := strings.IndexByte(p[i:], ']') + if end < 0 { + return strings.IndexByte(p, '?') + } + i += end + 1 + } + // Host and port data: everything up to a '/', '?', or ','. + for i < len(p) && p[i] != '/' && p[i] != '?' && p[i] != ',' { + i++ + } + if i < len(p) && p[i] == ',' { + i++ + continue + } + break + } + if i == len(p) { + return -1 + } + if p[i] == '?' { + return i } - if _, pwSet := u.User.Password(); pwSet { - u.User = url.UserPassword(u.User.Username(), "xxxxx") + // p[i] == '/': a path follows; the first '?' after it starts the query. + if j := strings.IndexByte(p[i:], '?'); j >= 0 { + return i + j } - return u.String() + return -1 } type NotPreferredError struct { diff --git a/vendor/github.com/jackc/pgx/v5/pgconn/parse_url.go b/vendor/github.com/jackc/pgx/v5/pgconn/parse_url.go new file mode 100644 index 0000000..3cc8d91 --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/pgconn/parse_url.go @@ -0,0 +1,397 @@ +package pgconn + +import ( + "errors" + "fmt" + "maps" + "strings" +) + +// This file parses PostgreSQL connection URIs using a parser designed to +// exactly match libpq's URI parser behavior (conninfo_uri_parse_options, +// conninfo_uri_parse_params, and conninfo_uri_decode in +// src/interfaces/libpq/fe-connect.c), so that pgx accepts and rejects exactly +// the same URIs as libpq, including libpq's multiple-host extension +// (postgresql://host1:port1,host2:port2/db), which is not valid RFC 3986 syntax. +// +// Deliberate differences from libpq: +// +// - Query parameters that libpq does not recognize are accepted. They become +// runtime parameters or pgx-specific options (e.g. pool_max_conns) instead +// of failing the parse. +// - Error messages avoid quoting the unredacted connection string and mask +// recognizable password fields on a best-effort basis. Invalid connection +// strings can be structurally ambiguous, so password redaction cannot be +// guaranteed for every malformed input. libpq quotes the failing input +// verbatim, which can leak a password into error messages and logs. +// - Literal NUL bytes are rejected. libpq never sees them because C strings +// end at the first NUL; a Go string can carry one into the startup packet, +// where it would act as a protocol delimiter and inject extra parameters. + +// parseURLMeta reports provenance facts about a parsed URI that +// ParseConfigWithOptions needs for ConnStringAllowedKeys validation and that +// cannot be recovered from the settings map itself. +type parseURLMeta struct { + // impliedEmptyPortList: the "port" entry is an all-empty list synthesized + // from the host list (postgres://h1,h2 yields port=",") with no port + // bytes supplied by the user. Determined from the raw port syntax, not + // the decoded value: a port of literal spaces (postgres://h: /db) + // decodes to empty but was user-supplied. + impliedEmptyPortList bool + // sawRawSSLKey / sawSSLTrueAlias / sawExplicitSSLModeKey record which + // spellings of the ssl and sslmode query keys appeared anywhere in the + // URI -- including occurrences later superseded under last-occurrence- + // wins. The ssl=true alias rewrite and ordinary repeated-key handling can + // remove those occurrences from the final settings map, so + // ConnStringAllowedKeys cannot fail closed from that map alone; it must see + // every key the user actually wrote. + sawRawSSLKey bool // key "ssl" with a value other than "true" + sawSSLTrueAlias bool // key "ssl" with value "true" (stored as sslmode=require) + sawExplicitSSLModeKey bool // key "sslmode" +} + +// parseURLSettings parses a connection URI into a settings map. connString +// must start with "postgres://" or "postgresql://". +// +// Multiple hosts are represented in the returned map the same way libpq +// represents them internally: settings["host"] and settings["port"] are +// comma-separated lists that are positionally aligned, with empty elements +// meaning "use the default". The lists are split in ParseConfigWithOptions. +func parseURLSettings(connString string) (settings map[string]string, meta parseURLMeta, err error) { + settings = make(map[string]string) + + if strings.IndexByte(connString, 0) >= 0 { + return nil, meta, errors.New("forbidden NUL byte in connection string") + } + + p, ok := strings.CutPrefix(connString, "postgresql://") + if !ok { + p, ok = strings.CutPrefix(connString, "postgres://") + } + if !ok { + return nil, meta, errors.New("invalid URI propagated to internal parser routine") + } + + // Look ahead for a possible user credentials designator. Like libpq, only + // a '/' stops the search, so a '@' anywhere before the path -- even inside + // what looks like a query string -- is treated as the userinfo terminator. + if i := strings.IndexAny(p, "@/"); i >= 0 && p[i] == '@' { + user, password, hasPassword := strings.Cut(p[:i], ":") + p = p[i+1:] + if user != "" { + val, err := uriDecode(user, "") + if err != nil { + return nil, meta, err + } + settings["user"] = val + } + if hasPassword && password != "" { + val, err := uriDecode(password, "password") + if err != nil { + return nil, meta, err + } + settings["password"] = val + } + } + + // Parse the comma-separated list of netloc[:port] specifications. Hosts + // and ports accumulate into positionally aligned comma-joined lists: every + // separator appends a comma to both, so postgres://h1,h2:5433 yields + // host="h1,h2" port=",5433". + var hostBuf, portBuf strings.Builder + + for { + if peekByte(p) == '[' { + // IPv6 address. The bracket contents are not validated, matching libpq. + end := strings.IndexByte(p, ']') + if end < 0 { + return nil, meta, errors.New(`end of string reached when looking for matching "]" in IPv6 host address in URI`) + } + if end == 1 { + return nil, meta, errors.New("IPv6 host address may not be empty in URI") + } + hostBuf.WriteString(p[1:end]) + p = p[end+1:] + if c := peekByte(p); c != 0 && c != ':' && c != '/' && c != '?' && c != ',' { + return nil, meta, fmt.Errorf(`unexpected character "%c" at position %d in URI (expected ":" or "/")`, c, len(connString)-len(p)+1) + } + } else { + // DNS-named or IPv4 netloc: everything up to a ':', '/', '?', or ','. + i := strings.IndexAny(p, ":/?,") + if i < 0 { + i = len(p) + } + hostBuf.WriteString(p[:i]) + p = p[i:] + } + + if peekByte(p) == ':' { + p = p[1:] + i := strings.IndexAny(p, "/?,") + if i < 0 { + i = len(p) + } + portBuf.WriteString(p[:i]) + p = p[i:] + } + + if peekByte(p) != ',' { + break + } + p = p[1:] + hostBuf.WriteByte(',') + portBuf.WriteByte(',') + } + + // The joined lists are percent-decoded as a unit, after joining. This + // means %2C decodes to a comma that later splits like a real separator -- + // a host name containing a literal comma is inexpressible, as in libpq. + if hostBuf.Len() > 0 { + hostList, err := uriDecode(hostBuf.String(), "") + if err != nil { + return nil, meta, err + } + settings["host"] = hostList + } + // Like libpq, the port list is stored even when every element is empty + // (postgres://h1,h2 yields port=","). This matters: a connection string + // port shadows PGPORT during settings merge, so with PGPORT=1,2 a + // three-host URI without ports must use the default port for every host, + // not fail the port/host count check. Such an implied all-empty list + // carries no user-supplied port text, which is what + // meta.impliedEmptyPortList reports -- judged on the raw bytes, before + // percent-decoding trims spaces, so a port the user actually typed never + // counts as implied even when it decodes to empty. + if portBuf.Len() > 0 { + meta.impliedEmptyPortList = strings.Trim(portBuf.String(), ",") == "" + portList, err := uriDecode(portBuf.String(), "") + if err != nil { + return nil, meta, err + } + settings["port"] = portList + } + + if peekByte(p) == '/' { + p = p[1:] + dbname := p + p = "" + if i := strings.IndexByte(dbname, '?'); i >= 0 { + p = dbname[i:] + dbname = dbname[:i] + } + // Like libpq, an empty path component leaves dbname unset so the + // default stays in effect. + if dbname != "" { + val, err := uriDecode(dbname, "") + if err != nil { + return nil, meta, err + } + settings["database"] = val + } + } + + if peekByte(p) == '?' { + // Query parameters land in their own map first: an explicit ?port= + // overrides the netloc-derived port list and is user-supplied port + // text, so it cancels the implied-list exemption even when its value + // is empty. + query := make(map[string]string) + if err := parseURLQueryParams(p[1:], query, &meta); err != nil { + return nil, meta, err + } + if _, ok := query["port"]; ok { + meta.impliedEmptyPortList = false + } + maps.Copy(settings, query) + } + + return settings, meta, nil +} + +// peekByte returns the first byte of s, or 0 if s is empty. A NUL byte is a +// safe end-of-input sentinel because parseURLSettings rejects literal NULs. +func peekByte(s string) byte { + if len(s) == 0 { + return 0 + } + return s[0] +} + +// parseURLQueryParams parses the query part of a connection URI into settings, +// mirroring libpq's conninfo_uri_parse_params: '&'-separated pairs, exactly +// one raw '=' per pair, both halves percent-decoded, last occurrence wins. It +// records in meta which spellings of the ssl/sslmode keys it saw (see +// parseURLMeta). +func parseURLQueryParams(params string, settings map[string]string, meta *parseURLMeta) error { + // sslWasLast records whether ssl or sslmode was encountered most recently. + // This matters if the final repeated ssl value is "true": the alias wins + // only when it occurs after the final explicit sslmode. + sslWasLast := false + + for params != "" { + pair := params + if i := strings.IndexByte(params, '&'); i >= 0 { + pair = params[:i] + params = params[i+1:] + } else { + params = "" + } + + rawKey, rawValue, found := strings.Cut(pair, "=") + if !found { + return fmt.Errorf(`missing key/value separator "=" in URI query parameter: "%s"`, rawKey) + } + if strings.IndexByte(rawValue, '=') >= 0 { + return fmt.Errorf(`extra key/value separator "=" in URI query parameter: "%s"`, rawKey) + } + + key, err := uriDecode(rawKey, "") + if err != nil { + return err + } + key = canonicalConnStringKey(key) + + secretName := "" + if key == "password" || key == "sslpassword" { + secretName = key + } + value, err := uriDecode(rawValue, secretName) + if err != nil { + return err + } + + // Resolve repeated ssl values under their raw key before applying the + // JDBC compatibility alias. Otherwise ssl=true would overwrite an + // independent explicit sslmode that must reappear if a later ssl value + // supersedes the alias. The alias is URI-only and applies only to the + // literal value "true". + switch key { + case "ssl": + sslWasLast = true + if value == "true" { + meta.sawSSLTrueAlias = true + } else { + meta.sawRawSSLKey = true + } + case "sslmode": + sslWasLast = false + meta.sawExplicitSSLModeKey = true + } + + settings[key] = value + } + + if value, ok := settings["ssl"]; ok && value == "true" { + delete(settings, "ssl") + if sslWasLast { + settings["sslmode"] = "require" + } + } + + return nil +} + +// uriDecode percent-decodes a URI component with the same rules as libpq's +// conninfo_uri_decode: '%' followed by exactly two case-insensitive hex +// digits, %00 forbidden, leading and trailing ASCII spaces skipped, interior +// spaces rejected. Decoded bytes are emitted raw with no character set +// validation. +// +// If secretName is non-empty, error messages identify that component by name +// instead of quoting its raw value. This prevents the known component's raw +// contents from being copied into the decode error; it does not make the +// separate, best-effort redaction of an invalid connection string complete. +func uriDecode(raw, secretName string) (string, error) { + // The component may be a secret; every error must be built by fail so the + // quote-the-raw-value-or-not redaction decision lives in exactly one place. + fail := func(secretFormat, publicFormat string) (string, error) { + if secretName != "" { + return "", fmt.Errorf(secretFormat, secretName) + } + return "", fmt.Errorf(publicFormat, raw) + } + + var b strings.Builder + b.Grow(len(raw)) + + i := 0 + for i < len(raw) && raw[i] == ' ' { + i++ + } + for i < len(raw) && raw[i] != ' ' { + if raw[i] != '%' { + b.WriteByte(raw[i]) + i++ + continue + } + + var hi, lo byte + var ok1, ok2 bool + if i+1 < len(raw) { + hi, ok1 = hexDigit(raw[i+1]) + } + if i+2 < len(raw) { + lo, ok2 = hexDigit(raw[i+2]) + } + if !ok1 || !ok2 { + return fail("invalid percent-encoded token in %s", `invalid percent-encoded token: "%s"`) + } + + c := hi<<4 | lo + if c == 0 { + return fail("forbidden value %%00 in percent-encoded value in %s", `forbidden value %%00 in percent-encoded value: "%s"`) + } + b.WriteByte(c) + i += 3 + } + for i < len(raw) && raw[i] == ' ' { + i++ + } + if i < len(raw) { + return fail("unexpected spaces found in %s, use percent-encoded spaces (%%20) instead", `unexpected spaces found in "%s", use percent-encoded spaces (%%20) instead`) + } + + return b.String(), nil +} + +// uriDecodeLenient is a non-failing variant of uriDecode used for best-effort +// redaction: valid percent-encodings are decoded, anything uriDecode would +// reject passes through unchanged. Redaction processing itself must not fail, +// and it must see the same key spelling the parser would (pass%77ord decodes +// to password), so encoded password keys are recognized even in connection +// strings that do not parse. This helps recognize keys but cannot make +// redaction complete when malformed input has ambiguous component boundaries. +func uriDecodeLenient(raw string) string { + raw = strings.Trim(raw, " ") + + var b strings.Builder + b.Grow(len(raw)) + + for i := 0; i < len(raw); { + if raw[i] == '%' && i+2 < len(raw) { + hi, ok1 := hexDigit(raw[i+1]) + lo, ok2 := hexDigit(raw[i+2]) + if ok1 && ok2 { + b.WriteByte(hi<<4 | lo) + i += 3 + continue + } + } + b.WriteByte(raw[i]) + i++ + } + + return b.String() +} + +func hexDigit(c byte) (byte, bool) { + switch { + case c >= '0' && c <= '9': + return c - '0', true + case c >= 'A' && c <= 'F': + return c - 'A' + 10, true + case c >= 'a' && c <= 'f': + return c - 'a' + 10, true + } + return 0, false +} diff --git a/vendor/github.com/jackc/pgx/v5/pgconn/pgconn.go b/vendor/github.com/jackc/pgx/v5/pgconn/pgconn.go index d181f7f..08bfd8a 100644 --- a/vendor/github.com/jackc/pgx/v5/pgconn/pgconn.go +++ b/vendor/github.com/jackc/pgx/v5/pgconn/pgconn.go @@ -13,6 +13,7 @@ import ( "maps" "math" "net" + "slices" "strconv" "strings" "sync" @@ -392,6 +393,9 @@ func connectOne(ctx context.Context, config *Config, connectConfig *connectOneCo pgConn.slowWriteTimer.Stop() pgConn.bgReaderStarted = make(chan struct{}) pgConn.frontend = config.BuildFrontend(pgConn.bgReader, pgConn.conn) + if config.MaxProtocolMessageBodyLen > 0 { + pgConn.frontend.SetMaxBodyLen(config.MaxProtocolMessageBodyLen) + } startupMsg := pgproto3.StartupMessage{ ProtocolVersion: maxProtocolVersion, @@ -469,13 +473,7 @@ func connectOne(ctx context.Context, config *Config, connectConfig *connectOneCo clientFinishedAuth = true case *pgproto3.AuthenticationSASL: // Check if OAUTHBEARER is supported - serverSupportsOAuthBearer := false - for _, mech := range msg.AuthMechanisms { - if mech == "OAUTHBEARER" { - serverSupportsOAuthBearer = true - break - } - } + serverSupportsOAuthBearer := slices.Contains(msg.AuthMechanisms, "OAUTHBEARER") if serverSupportsOAuthBearer && pgConn.config.OAuthTokenProvider != nil { if err := requireAuthPolicy.check(authMethodOAuth); err != nil { @@ -574,10 +572,10 @@ func (pgConn *PgConn) signalMessage() chan struct{} { panic("BUG: signalMessage when already in progress") } + ch := make(chan struct{}) pgConn.bufferingReceive = true pgConn.bufferingReceiveMux.Lock() - ch := make(chan struct{}) go func() { pgConn.bufferingReceiveMsg, pgConn.bufferingReceiveErr = pgConn.frontend.Receive() pgConn.bufferingReceiveMux.Unlock() @@ -790,6 +788,16 @@ func (pgConn *PgConn) asyncClose() { pgConn.frontend.Send(&pgproto3.Terminate{}) pgConn.flushWithPotentialWriteReadDeadlock() + + // Drain any data already in flight from the server (DataRows that were sent before the + // CancelRequest landed, the resulting ErrorResponse, ReadyForQuery, and finally the server's + // own close after it processes Terminate). Closing a TCP socket while unread data remains in + // the kernel receive buffer causes the OS to send RST instead of FIN, which surfaces on the + // server or proxy as "connection reset by peer". The deadline set above bounds how long this + // will block; on timeout we fall through to Close() and accept the abortive close. + // + // See https://github.com/jackc/pgx/issues/2584 + io.Copy(io.Discard, pgConn.conn) }() } @@ -1295,7 +1303,9 @@ func (pgConn *PgConn) ExecPrepared(ctx context.Context, stmtName string, paramVa // // This differs from [PgConn.ExecPrepared] in that it takes a [*StatementDescription] instead of the prepared statement name. // Because it has the [*StatementDescription] it can avoid the Describe Portal message that [PgConn.ExecPrepared] must send to get -// the result column descriptions. +// the result column descriptions. However, if the statement description has no fields then a Describe is still sent, as +// an empty Fields may mean the results were not knowable at prepare time, e.g. a FETCH from a cursor that did not exist +// yet. // // paramValues are the parameter values. It must be encoded in the format given by paramFormats. // @@ -1356,7 +1366,10 @@ func (pgConn *PgConn) execExtendedPrefix(ctx context.Context, paramValues [][]by } func (pgConn *PgConn) execExtendedSuffix(result *ResultReader, statementDescription *StatementDescription, resultFormats []int16) { - if statementDescription == nil { + if statementDescription == nil || len(statementDescription.Fields) == 0 { + // The cached field descriptions are missing or empty. Empty field descriptions can occur when the statement's + // result set was not known at prepare time, e.g. a FETCH from a cursor that did not exist yet. Send a Describe + // so the server supplies the actual row description when the statement is executed. pgConn.frontend.SendDescribe(&pgproto3.Describe{ObjectType: 'P'}) } pgConn.frontend.SendExecute(&pgproto3.Execute{}) @@ -1572,10 +1585,15 @@ type MultiResultReader struct { rr *ResultReader - // Data from when the batch was queued. + // Data from when the batch was queued. There is one entry per command in the batch. Entries are nil for commands + // other than Batch.ExecStatement, which is the only command that does not request a RowDescription from the server. statementDescriptions []*StatementDescription resultFormats [][]int16 + // Statement data for the command currently being processed. Popped from the queues above at each BindComplete. + currentStatementDescription *StatementDescription + currentResultFormats []int16 + closed bool err error } @@ -1619,19 +1637,19 @@ func (mrr *MultiResultReader) NextResult() bool { for !mrr.closed && mrr.err == nil { msg, _ := mrr.pgConn.peekMessage() if _, ok := msg.(*pgproto3.DataRow); ok { - if len(mrr.statementDescriptions) > 0 { + if sd := mrr.currentStatementDescription; sd != nil { rr := ResultReader{ pgConn: mrr.pgConn, multiResultReader: mrr, ctx: mrr.ctx, } - // This result corresponds to a prepared statement description that was provided when queuing the batch. - sd := mrr.statementDescriptions[0] - mrr.statementDescriptions = mrr.statementDescriptions[1:] - - resultFormats := mrr.resultFormats[0] - mrr.resultFormats = mrr.resultFormats[1:] + // This result corresponds to a Batch.ExecStatement command. No RowDescription was requested from the + // server so the field descriptions come from the statement description that was provided when queuing + // the batch. + resultFormats := mrr.currentResultFormats + mrr.currentStatementDescription = nil + mrr.currentResultFormats = nil sdFields := sd.Fields rr.fieldDescriptions = rr.pgConn.getFieldDescriptionSlice(len(sdFields)) @@ -1656,7 +1674,24 @@ func (mrr *MultiResultReader) NextResult() bool { } switch msg := msg.(type) { + case *pgproto3.BindComplete: + // Every command in a batch begins with a BindComplete. Pop this command's statement data so that the + // following messages are matched with the correct statement description. It must be popped here rather than + // when a DataRow is peeked because a command that returns no rows would otherwise leave its entry in the + // queue, misaligning the statement descriptions for all subsequent commands. + if len(mrr.statementDescriptions) > 0 { + mrr.currentStatementDescription = mrr.statementDescriptions[0] + mrr.statementDescriptions = mrr.statementDescriptions[1:] + mrr.currentResultFormats = mrr.resultFormats[0] + mrr.resultFormats = mrr.resultFormats[1:] + } else { + mrr.currentStatementDescription = nil + mrr.currentResultFormats = nil + } case *pgproto3.RowDescription: + mrr.currentStatementDescription = nil + mrr.currentResultFormats = nil + mrr.pgConn.resultReader = ResultReader{ pgConn: mrr.pgConn, multiResultReader: mrr, @@ -1668,11 +1703,25 @@ func (mrr *MultiResultReader) NextResult() bool { mrr.rr = &mrr.pgConn.resultReader return true case *pgproto3.CommandComplete: - mrr.pgConn.resultReader = ResultReader{ + rr := ResultReader{ commandTag: mrr.pgConn.makeCommandTag(msg.CommandTag), commandConcluded: true, closed: true, } + + if sd := mrr.currentStatementDescription; sd != nil { + // A Batch.ExecStatement command that returned no rows. Attach the field descriptions from the statement + // description so the result reports its columns the same as a result that was described by the server. + rr.fieldDescriptions = mrr.pgConn.getFieldDescriptionSlice(len(sd.Fields)) + err := combineFieldDescriptionsAndResultFormats(rr.fieldDescriptions, sd.Fields, mrr.currentResultFormats) + if err != nil { + rr.err = err + } + mrr.currentStatementDescription = nil + mrr.currentResultFormats = nil + } + + mrr.pgConn.resultReader = rr mrr.rr = &mrr.pgConn.resultReader return true case *pgproto3.EmptyQueryResponse: @@ -1953,6 +2002,11 @@ func (batch *Batch) ExecPrepared(stmtName string, paramValues [][]byte, paramFor return } + // The statement data queues must have one entry per command so results can be matched with the correct statement + // description. This command requests a RowDescription from the server so it queues a nil placeholder. + batch.statementDescriptions = append(batch.statementDescriptions, nil) + batch.resultFormats = append(batch.resultFormats, nil) + batch.buf, batch.err = (&pgproto3.Describe{ObjectType: 'P'}).Encode(batch.buf) if batch.err != nil { return @@ -1968,7 +2022,9 @@ func (batch *Batch) ExecPrepared(stmtName string, paramValues [][]byte, paramFor // // This differs from ExecPrepared in that it takes a *StatementDescription instead of just the prepared statement name. // Because it has the *StatementDescription it can avoid the Describe Portal message that ExecPrepared must send to get -// the result column descriptions. +// the result column descriptions. However, if the statement description has no fields then a Describe is still sent, as +// an empty Fields may mean the results were not knowable at prepare time, e.g. a FETCH from a cursor that did not exist +// yet. func (batch *Batch) ExecStatement(statementDescription *StatementDescription, paramValues [][]byte, paramFormats, resultFormats []int16) { if batch.err != nil { return @@ -1979,6 +2035,16 @@ func (batch *Batch) ExecStatement(statementDescription *StatementDescription, pa return } + if len(statementDescription.Fields) == 0 { + // The cached field descriptions are empty, which can occur when the statement's result set was not known at + // prepare time, e.g. a FETCH from a cursor that did not exist yet. Send a Describe so the server supplies the + // actual row description when the statement is executed. + batch.buf, batch.err = (&pgproto3.Describe{ObjectType: 'P'}).Encode(batch.buf) + if batch.err != nil { + return + } + } + batch.statementDescriptions = append(batch.statementDescriptions, statementDescription) batch.resultFormats = append(batch.resultFormats, resultFormats) @@ -2242,6 +2308,9 @@ func Construct(hc *HijackedConn) (*PgConn, error) { pgConn.slowWriteTimer.Stop() pgConn.bgReaderStarted = make(chan struct{}) pgConn.frontend = hc.Config.BuildFrontend(pgConn.bgReader, pgConn.conn) + if hc.Config.MaxProtocolMessageBodyLen > 0 { + pgConn.frontend.SetMaxBodyLen(hc.Config.MaxProtocolMessageBodyLen) + } return pgConn, nil } @@ -2512,6 +2581,12 @@ func (p *Pipeline) SendQueryStatement(statementDescription *StatementDescription } p.conn.frontend.SendBind(&pgproto3.Bind{PreparedStatement: statementDescription.Name, ParameterFormatCodes: paramFormats, Parameters: paramValues, ResultFormatCodes: resultFormats}) + if len(statementDescription.Fields) == 0 { + // The cached field descriptions are empty. This can occur when the statement's result set is + // not known at prepare time, e.g. a FETCH from a cursor that did not exist yet. Send a + // Describe so the server supplies the actual row description when the statement is executed. + p.conn.frontend.SendDescribe(&pgproto3.Describe{ObjectType: 'P'}) + } p.conn.frontend.SendExecute(&pgproto3.Execute{}) p.state.PushBackRequestType(pipelineQueryStatement) p.state.PushBackStatementData(statementDescription, resultFormats) @@ -2607,22 +2682,29 @@ func (p *Pipeline) getResults() (results any, err error) { case pipelineNil: return nil, nil case pipelinePrepare: - return p.getResultsPrepare() + results, err = p.getResultsPrepare() case pipelineQueryParams: - return p.getResultsQueryParams() + results, err = p.getResultsQueryParams() case pipelineQueryPrepared: - return p.getResultsQueryPrepared() + results, err = p.getResultsQueryPrepared() case pipelineQueryStatement: - return p.getResultsQueryStatement() + results, err = p.getResultsQueryStatement() case pipelineDeallocate: - return p.getResultsDeallocate() + results, err = p.getResultsDeallocate() case pipelineSyncRequest: - return p.getResultsSync() + results, err = p.getResultsSync() case pipelineFlushRequest: return nil, errors.New("BUG: pipelineFlushRequest should not be in request queue") default: return nil, errors.New("BUG: unknown pipeline request type") } + + if err != nil { + // Return an untyped nil instead of an interface containing a typed nil pointer so that callers can compare + // results to nil. + return nil, err + } + return results, nil } func (p *Pipeline) getResultsPrepare() (*StatementDescription, error) { @@ -2699,21 +2781,48 @@ func (p *Pipeline) getResultsQueryPrepared() (*ResultReader, error) { } func (p *Pipeline) getResultsQueryStatement() (*ResultReader, error) { + // The statement data must be extracted even if an error occurs. Otherwise, it would still be in the queue and + // subsequent QueryStatement results would be misaligned with their statement descriptions. + sd, resultFormats := p.state.ExtractFrontStatementData() + if sd == nil { + return nil, errors.New("BUG: missing statement description or result formats for QueryStatement") + } + err := p.receiveBindComplete("QueryStatement") if err != nil { return nil, err } + sdFields := sd.Fields + if len(sdFields) == 0 { + // A Describe was sent for this statement (see SendQueryStatement). Read the server-provided + // row description which may include fields that were not known at prepare time. + msg, err := p.receiveMessage() + if err != nil { + return nil, err + } + + switch msg := msg.(type) { + case *pgproto3.RowDescription: + sdFields = make([]FieldDescription, len(msg.Fields)) + convertRowDescription(sdFields, msg) + case *pgproto3.NoData: + // Statement returns no rows. + case *pgproto3.ErrorResponse: + pgErr := ErrorResponseToPgError(msg) + p.state.HandleError(pgErr) + p.conn.resultReader.closed = true + return nil, pgErr + default: + return nil, p.handleUnexpectedMessage("QueryStatement RowDescription or NoData", msg) + } + } + msg, err := p.receiveMessage() if err != nil { return nil, err } - sd, resultFormats := p.state.ExtractFrontStatementData() - if sd == nil { - return nil, errors.New("BUG: missing statement description or result formats for QueryStatement") - } - sdFields := sd.Fields fieldDescriptions := p.conn.getFieldDescriptionSlice(len(sdFields)) err = combineFieldDescriptionsAndResultFormats(fieldDescriptions, sdFields, resultFormats) if err != nil { @@ -2739,6 +2848,12 @@ func (p *Pipeline) getResultsQueryStatement() (*ResultReader, error) { fieldDescriptions: fieldDescriptions, } return &p.conn.resultReader, nil + case *pgproto3.EmptyQueryResponse: + p.conn.resultReader = ResultReader{ + commandConcluded: true, + closed: true, + } + return &p.conn.resultReader, nil case *pgproto3.ErrorResponse: pgErr := ErrorResponseToPgError(msg) p.state.HandleError(pgErr) @@ -2866,6 +2981,15 @@ func (p *Pipeline) receiveDescribedResultReader(errStr string) (*ResultReader, e closed: true, } return &p.conn.resultReader, nil + + // EmptyQueryResponse is returned instead of CommandComplete when the query is empty. e.g. A comment-only query. + case *pgproto3.EmptyQueryResponse: + p.conn.resultReader = ResultReader{ + commandConcluded: true, + closed: true, + } + return &p.conn.resultReader, nil + case *pgproto3.ErrorResponse: pgErr := ErrorResponseToPgError(msg) p.state.HandleError(pgErr) diff --git a/vendor/github.com/jackc/pgx/v5/pgproto3/negotiate_protocol_version.go b/vendor/github.com/jackc/pgx/v5/pgproto3/negotiate_protocol_version.go index 43bd7ec..936576a 100644 --- a/vendor/github.com/jackc/pgx/v5/pgproto3/negotiate_protocol_version.go +++ b/vendor/github.com/jackc/pgx/v5/pgproto3/negotiate_protocol_version.go @@ -34,7 +34,7 @@ func (dst *NegotiateProtocolVersion) Decode(src []byte) error { capHint = remaining } dst.UnrecognizedOptions = make([]string, 0, capHint) - for i := 0; i < optionCount; i++ { + for range optionCount { if rp >= len(src) { return &invalidMessageFormatErr{messageType: "NegotiateProtocolVersion"} } diff --git a/vendor/github.com/jackc/pgx/v5/pgproto3/startup_message.go b/vendor/github.com/jackc/pgx/v5/pgproto3/startup_message.go index eb48f72..69cf99a 100644 --- a/vendor/github.com/jackc/pgx/v5/pgproto3/startup_message.go +++ b/vendor/github.com/jackc/pgx/v5/pgproto3/startup_message.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "github.com/jackc/pgx/v5/internal/pgio" ) @@ -75,6 +76,24 @@ func (src *StartupMessage) Encode(dst []byte) ([]byte, error) { dst = pgio.AppendUint32(dst, src.ProtocolVersion) for k, v := range src.Parameters { + // The startup message body is a run of NUL-delimited strings whose + // length is data-driven: the server keeps reading name/value pairs + // until the empty name that terminates the list. Other messages have a + // field count fixed by the message type, so a stray NUL there leaves + // trailing bytes and the server rejects the message; here it simply + // yields more parameters. A libpq caller cannot reach this state + // because its parameters are NUL-terminated C strings, but a Go string + // can carry a NUL, so an application_name of "x\x00user\x00admin" + // would silently change the role the connection logs in as. Refuse to + // encode instead. + if strings.IndexByte(k, 0) >= 0 { + return nil, errors.New("startup message parameter name contains NUL byte") + } + if strings.IndexByte(v, 0) >= 0 { + // Name the parameter but not the value: values can hold secrets. + return nil, fmt.Errorf("startup message parameter %q contains NUL byte in value", k) + } + dst = append(dst, k...) dst = append(dst, 0) dst = append(dst, v...) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/array.go b/vendor/github.com/jackc/pgx/v5/pgtype/array.go index 26505fb..64a8498 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/array.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/array.go @@ -2,7 +2,6 @@ package pgtype import ( "bytes" - "encoding/binary" "fmt" "io" "strconv" @@ -45,39 +44,30 @@ func cardinality(dimensions []ArrayDimension) int { return elementCount } -func (dst *arrayHeader) DecodeBinary(m *Map, src []byte) (int, error) { - if len(src) < 12 { - return 0, fmt.Errorf("array header too short: %d", len(src)) +func (dst *arrayHeader) DecodeBinary(r *pgio.Reader) error { + // Each dimension is 8 bytes, which also bounds the Dimensions allocation below. + numDims := r.Count(8) + if err := r.Err(); err != nil { + return fmt.Errorf("array header: %w", err) } - rp := 0 - - numDims := int(binary.BigEndian.Uint32(src[rp:])) - rp += 4 - if numDims > 6 { - return 0, fmt.Errorf("array has too many dimensions: %d", numDims) + return fmt.Errorf("array has too many dimensions: %d", numDims) } - dst.ContainsNull = binary.BigEndian.Uint32(src[rp:]) == 1 - rp += 4 - - dst.ElementOID = binary.BigEndian.Uint32(src[rp:]) - rp += 4 + dst.ContainsNull = r.Uint32() == 1 + dst.ElementOID = r.Uint32() - if len(src) < 12+numDims*8 { - return 0, fmt.Errorf("array header too short for %d dimensions: %d", numDims, len(src)) - } dst.Dimensions = make([]ArrayDimension, numDims) for i := range dst.Dimensions { - dst.Dimensions[i].Length = int32(binary.BigEndian.Uint32(src[rp:])) - rp += 4 - - dst.Dimensions[i].LowerBound = int32(binary.BigEndian.Uint32(src[rp:])) - rp += 4 + dst.Dimensions[i].Length = r.Int32() + dst.Dimensions[i].LowerBound = r.Int32() } - return rp, nil + if err := r.Err(); err != nil { + return fmt.Errorf("array header: %w", err) + } + return nil } func (src arrayHeader) EncodeBinary(buf []byte) []byte { @@ -105,7 +95,11 @@ type untypedTextArray struct { Dimensions []ArrayDimension } -func parseUntypedTextArray(src string) (*untypedTextArray, error) { +func parseUntypedTextArray(src string, delimiter byte) (*untypedTextArray, error) { + if delimiter == 0 { + delimiter = ',' + } + dst := &untypedTextArray{ Elements: []string{}, Quoted: []bool{}, @@ -212,7 +206,7 @@ func parseUntypedTextArray(src string) (*untypedTextArray, error) { implicitDimensions[currentDim].Length++ } currentDim++ - case ',': + case rune(delimiter): case '}': currentDim-- if currentDim < counterDim { @@ -220,7 +214,7 @@ func parseUntypedTextArray(src string) (*untypedTextArray, error) { } default: buf.UnreadRune() - value, quoted, err := arrayParseValue(buf) + value, quoted, err := arrayParseValue(buf, delimiter) if err != nil { return nil, fmt.Errorf("invalid array value: %w", err) } @@ -264,7 +258,7 @@ func skipWhitespace(buf *bytes.Buffer) { } } -func arrayParseValue(buf *bytes.Buffer) (string, bool, error) { +func arrayParseValue(buf *bytes.Buffer, delimiter byte) (string, bool, error) { r, _, err := buf.ReadRune() if err != nil { return "", false, err @@ -283,7 +277,7 @@ func arrayParseValue(buf *bytes.Buffer) (string, bool, error) { } switch r { - case ',', '}': + case rune(delimiter), '}': buf.UnreadRune() return s.String(), false, nil } @@ -370,14 +364,12 @@ func quoteArrayElement(src string) string { return `"` + quoteArrayReplacer.Replace(src) + `"` } -func isSpace(ch byte) bool { - // see array_isspace: - // https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/arrayfuncs.c - return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\v' || ch == '\f' -} +func quoteArrayElementIfNeeded(src string, delimiter byte) string { + if delimiter == 0 { + delimiter = ',' + } -func quoteArrayElementIfNeeded(src string) string { - if src == "" || (len(src) == 4 && strings.EqualFold(src, "null")) || isSpace(src[0]) || isSpace(src[len(src)-1]) || strings.ContainsAny(src, `{},"\`) { + if src == "" || (len(src) == 4 && strings.EqualFold(src, "null")) || strings.ContainsAny(src, " \t\n\r\v\f") || strings.ContainsAny(src, `{},"\`) || strings.ContainsRune(src, rune(delimiter)) { return quoteArrayElement(src) } return src diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/array_codec.go b/vendor/github.com/jackc/pgx/v5/pgtype/array_codec.go index ac01496..1c9acc9 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/array_codec.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/array_codec.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "reflect" @@ -39,6 +38,16 @@ type ArraySetter interface { // ArrayCodec is a codec for any array type. type ArrayCodec struct { ElementType *Type + // Delimiter is the character PostgreSQL uses to separate array elements for this type. + // If unset, "," is used. + Delimiter byte +} + +func (c *ArrayCodec) delimiter() byte { + if c.Delimiter == 0 { + return ',' + } + return c.Delimiter } func (c *ArrayCodec) FormatSupported(format int16) bool { @@ -118,9 +127,10 @@ func (p *encodePlanArrayCodecText) Encode(value any, buf []byte) (newBuf []byte, var encodePlan EncodePlan var lastElemType reflect.Type inElemBuf := make([]byte, 0, 32) + delimiter := p.ac.delimiter() for i := range elementCount { if i > 0 { - buf = append(buf, ',') + buf = append(buf, delimiter) } for _, dec := range dimElemCounts { @@ -155,7 +165,7 @@ func (p *encodePlanArrayCodecText) Encode(value any, buf []byte) (newBuf []byte, if elemBuf == nil { buf = append(buf, `NULL`...) } else { - buf = append(buf, quoteArrayElementIfNeeded(string(elemBuf))...) + buf = append(buf, quoteArrayElementIfNeeded(string(elemBuf), delimiter)...) } for _, dec := range dimElemCounts { @@ -261,8 +271,9 @@ func (c *ArrayCodec) PlanScan(m *Map, oid uint32, format int16, target any) Scan } func (c *ArrayCodec) decodeBinary(m *Map, arrayOID uint32, src []byte, array ArraySetter) error { + r := pgio.NewReader(src) var arrayHeader arrayHeader - rp, err := arrayHeader.DecodeBinary(m, src) + err := arrayHeader.DecodeBinary(r) if err != nil { return err } @@ -271,8 +282,8 @@ func (c *ArrayCodec) decodeBinary(m *Map, arrayOID uint32, src []byte, array Arr // Each element carries at minimum a 4-byte length header, so elementCount cannot exceed the // remaining bytes / 4. This bounds the allocation in SetDimensions and the loop below against a // malicious server claiming huge dimensions in a small message. - if maxElements := len(src[rp:]) / 4; elementCount > maxElements { - return fmt.Errorf("array claims %d elements but only %d bytes remain", elementCount, len(src[rp:])) + if maxElements := r.Remaining() / 4; elementCount > maxElements { + return fmt.Errorf("array claims %d elements but only %d bytes remain", elementCount, r.Remaining()) } err = array.SetDimensions(arrayHeader.Dimensions) @@ -281,7 +292,7 @@ func (c *ArrayCodec) decodeBinary(m *Map, arrayOID uint32, src []byte, array Arr } if elementCount == 0 { - return nil + return r.Finish() } elementScanPlan := c.ElementType.Codec.PlanScan(m, c.ElementType.OID, BinaryFormatCode, array.ScanIndex(0)) @@ -290,19 +301,10 @@ func (c *ArrayCodec) decodeBinary(m *Map, arrayOID uint32, src []byte, array Arr } for i := range elementCount { - if len(src[rp:]) < 4 { - return fmt.Errorf("array body truncated at element %d", i) - } elem := array.ScanIndex(i) - elemLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) - rp += 4 - var elemSrc []byte - if elemLen >= 0 { - if len(src[rp:]) < elemLen { - return fmt.Errorf("array element %d length %d exceeds remaining %d bytes", i, elemLen, len(src[rp:])) - } - elemSrc = src[rp : rp+elemLen] - rp += elemLen + elemSrc, _ := r.Value() + if err := r.Err(); err != nil { + return fmt.Errorf("array element %d: %w", i, err) } err = elementScanPlan.Scan(elemSrc, elem) if err != nil { @@ -310,15 +312,21 @@ func (c *ArrayCodec) decodeBinary(m *Map, arrayOID uint32, src []byte, array Arr } } - return nil + return r.Finish() } func (c *ArrayCodec) decodeText(m *Map, arrayOID uint32, src []byte, array ArraySetter) error { - uta, err := parseUntypedTextArray(string(src)) + uta, err := parseUntypedTextArray(string(src), c.delimiter()) if err != nil { return err } + // The element loop below indexes the value sized by SetDimensions, so the + // dimensions and the parsed elements must agree. + if elementCount := cardinality(uta.Dimensions); elementCount != len(uta.Elements) { + return fmt.Errorf("array dimensions describe %d elements but %d were parsed", elementCount, len(uta.Elements)) + } + err = array.SetDimensions(uta.Dimensions) if err != nil { return err diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/bits.go b/vendor/github.com/jackc/pgx/v5/pgtype/bits.go index 986fe23..052c157 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/bits.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/bits.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "github.com/jackc/pgx/v5/internal/pgio" @@ -162,20 +161,31 @@ func (c BitsCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (an type scanPlanBinaryBitsToBitsScanner struct{} func (scanPlanBinaryBitsToBitsScanner) Scan(src []byte, dst any) error { - scanner := (dst).(BitsScanner) + scanner := dst.(BitsScanner) if src == nil { return scanner.ScanBits(Bits{}) } - if len(src) < 4 { - return fmt.Errorf("invalid length for bit/varbit: %v", len(src)) + r := pgio.NewReader(src) + + bitLen := r.Int32() + if err := r.Err(); err != nil { + return fmt.Errorf("invalid length for bit/varbit: %w", err) + } + if bitLen < 0 { + return fmt.Errorf("invalid length for bit/varbit: bitLen=%d", bitLen) + } + + // Finish rejects trailing bytes, so together with the read below the data + // must be exactly the number of bytes bitLen calls for. + data := r.Bytes((int(bitLen) + 7) / 8) + if err := r.Finish(); err != nil { + return fmt.Errorf("invalid length for bit/varbit: bitLen=%d: %w", bitLen, err) } - bitLen := int32(binary.BigEndian.Uint32(src)) - rp := 4 - buf := make([]byte, len(src[rp:])) - copy(buf, src[rp:]) + buf := make([]byte, len(data)) + copy(buf, data) return scanner.ScanBits(Bits{Bytes: buf, Len: bitLen, Valid: true}) } @@ -183,7 +193,7 @@ func (scanPlanBinaryBitsToBitsScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToBitsScanner struct{} func (scanPlanTextAnyToBitsScanner) Scan(src []byte, dst any) error { - scanner := (dst).(BitsScanner) + scanner := dst.(BitsScanner) if src == nil { return scanner.ScanBits(Bits{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/bool.go b/vendor/github.com/jackc/pgx/v5/pgtype/bool.go index 077668e..986ae19 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/bool.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/bool.go @@ -252,7 +252,7 @@ func (scanPlanBinaryBoolToBool) Scan(src []byte, dst any) error { return fmt.Errorf("invalid length for bool: %v", len(src)) } - p, ok := (dst).(*bool) + p, ok := dst.(*bool) if !ok { return ErrScanTargetTypeChanged } @@ -273,7 +273,7 @@ func (scanPlanTextAnyToBool) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan empty string into %T", dst) } - p, ok := (dst).(*bool) + p, ok := dst.(*bool) if !ok { return ErrScanTargetTypeChanged } @@ -291,7 +291,7 @@ func (scanPlanTextAnyToBool) Scan(src []byte, dst any) error { type scanPlanBinaryBoolToBoolScanner struct{} func (scanPlanBinaryBoolToBoolScanner) Scan(src []byte, dst any) error { - s, ok := (dst).(BoolScanner) + s, ok := dst.(BoolScanner) if !ok { return ErrScanTargetTypeChanged } @@ -310,7 +310,7 @@ func (scanPlanBinaryBoolToBoolScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToBoolScanner struct{} func (scanPlanTextAnyToBoolScanner) Scan(src []byte, dst any) error { - s, ok := (dst).(BoolScanner) + s, ok := dst.(BoolScanner) if !ok { return ErrScanTargetTypeChanged } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/box.go b/vendor/github.com/jackc/pgx/v5/pgtype/box.go index 8270aaf..d05bef2 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/box.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/box.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "math" "strconv" @@ -145,20 +144,22 @@ func (BoxCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan type scanPlanBinaryBoxToBoxScanner struct{} func (scanPlanBinaryBoxToBoxScanner) Scan(src []byte, dst any) error { - scanner := (dst).(BoxScanner) + scanner := dst.(BoxScanner) if src == nil { return scanner.ScanBox(Box{}) } - if len(src) != 32 { - return fmt.Errorf("invalid length for Box: %v", len(src)) - } + r := pgio.NewReader(src) - x1 := binary.BigEndian.Uint64(src) - y1 := binary.BigEndian.Uint64(src[8:]) - x2 := binary.BigEndian.Uint64(src[16:]) - y2 := binary.BigEndian.Uint64(src[24:]) + x1 := r.Uint64() + y1 := r.Uint64() + x2 := r.Uint64() + y2 := r.Uint64() + + if err := r.Finish(); err != nil { + return fmt.Errorf("Box: %w", err) + } return scanner.ScanBox(Box{ P: [2]Vec2{ @@ -172,7 +173,7 @@ func (scanPlanBinaryBoxToBoxScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToBoxScanner struct{} func (scanPlanTextAnyToBoxScanner) Scan(src []byte, dst any) error { - scanner := (dst).(BoxScanner) + scanner := dst.(BoxScanner) if src == nil { return scanner.ScanBox(Box{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/builtin_wrappers.go b/vendor/github.com/jackc/pgx/v5/pgtype/builtin_wrappers.go index a412763..bfae662 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/builtin_wrappers.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/builtin_wrappers.go @@ -599,7 +599,7 @@ func (w *netipAddrWrapper) ScanNetipPrefix(v netip.Prefix) error { } func (w netipAddrWrapper) NetipPrefixValue() (netip.Prefix, error) { - addr := (netip.Addr)(w) + addr := netip.Addr(w) if !addr.IsValid() { return netip.Prefix{}, nil } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/bytea.go b/vendor/github.com/jackc/pgx/v5/pgtype/bytea.go index 6c4f0c5..32c026e 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/bytea.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/bytea.go @@ -185,7 +185,7 @@ func (scanPlanBinaryBytesToBytes) Scan(src []byte, dst any) error { type scanPlanBinaryBytesToBytesScanner struct{} func (scanPlanBinaryBytesToBytesScanner) Scan(src []byte, dst any) error { - scanner := (dst).(BytesScanner) + scanner := dst.(BytesScanner) return scanner.ScanBytes(src) } @@ -210,7 +210,7 @@ func (scanPlanTextByteaToBytes) Scan(src []byte, dst any) error { type scanPlanTextByteaToBytesScanner struct{} func (scanPlanTextByteaToBytesScanner) Scan(src []byte, dst any) error { - scanner := (dst).(BytesScanner) + scanner := dst.(BytesScanner) buf, err := decodeHexBytea(src) if err != nil { return err diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/circle.go b/vendor/github.com/jackc/pgx/v5/pgtype/circle.go index ea1d629..c72c2fe 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/circle.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/circle.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "math" "strconv" @@ -161,19 +160,21 @@ func (c CircleCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) ( type scanPlanBinaryCircleToCircleScanner struct{} func (scanPlanBinaryCircleToCircleScanner) Scan(src []byte, dst any) error { - scanner := (dst).(CircleScanner) + scanner := dst.(CircleScanner) if src == nil { return scanner.ScanCircle(Circle{}) } - if len(src) != 24 { - return fmt.Errorf("invalid length for Circle: %v", len(src)) - } + rd := pgio.NewReader(src) - x := binary.BigEndian.Uint64(src) - y := binary.BigEndian.Uint64(src[8:]) - r := binary.BigEndian.Uint64(src[16:]) + x := rd.Uint64() + y := rd.Uint64() + r := rd.Uint64() + + if err := rd.Finish(); err != nil { + return fmt.Errorf("Circle: %w", err) + } return scanner.ScanCircle(Circle{ P: Vec2{math.Float64frombits(x), math.Float64frombits(y)}, @@ -185,7 +186,7 @@ func (scanPlanBinaryCircleToCircleScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToCircleScanner struct{} func (scanPlanTextAnyToCircleScanner) Scan(src []byte, dst any) error { - scanner := (dst).(CircleScanner) + scanner := dst.(CircleScanner) if src == nil { return scanner.ScanCircle(Circle{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/composite.go b/vendor/github.com/jackc/pgx/v5/pgtype/composite.go index 7f96ab4..ae1b6eb 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/composite.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/composite.go @@ -130,7 +130,7 @@ type scanPlanBinaryCompositeToCompositeIndexScanner struct { } func (plan *scanPlanBinaryCompositeToCompositeIndexScanner) Scan(src []byte, target any) error { - targetScanner := (target).(CompositeIndexScanner) + targetScanner := target.(CompositeIndexScanner) if src == nil { return targetScanner.ScanNull() @@ -139,14 +139,17 @@ func (plan *scanPlanBinaryCompositeToCompositeIndexScanner) Scan(src []byte, tar scanner := NewCompositeBinaryScanner(plan.m, src) for i, field := range plan.cc.Fields { if scanner.Next() { - fieldTarget := targetScanner.ScanIndex(i) + fieldTarget, err := compositeFieldTarget(targetScanner, i) + if err != nil { + return err + } if fieldTarget != nil { fieldPlan := plan.m.PlanScan(field.Type.OID, BinaryFormatCode, fieldTarget) if fieldPlan == nil { return fmt.Errorf("unable to encode %v into OID %d in binary format", field, field.Type.OID) } - err := fieldPlan.Scan(scanner.Bytes(), fieldTarget) + err = fieldPlan.Scan(scanner.Bytes(), fieldTarget) if err != nil { return err } @@ -169,7 +172,7 @@ type scanPlanTextCompositeToCompositeIndexScanner struct { } func (plan *scanPlanTextCompositeToCompositeIndexScanner) Scan(src []byte, target any) error { - targetScanner := (target).(CompositeIndexScanner) + targetScanner := target.(CompositeIndexScanner) if src == nil { return targetScanner.ScanNull() @@ -277,9 +280,8 @@ func (c *CompositeCodec) DecodeValue(m *Map, oid uint32, format int16, src []byt } type CompositeBinaryScanner struct { - m *Map - rp int - src []byte + m *Map + r *pgio.Reader fieldCount int32 fieldBytes []byte @@ -289,19 +291,18 @@ type CompositeBinaryScanner struct { // NewCompositeBinaryScanner a scanner over a binary encoded composite value. func NewCompositeBinaryScanner(m *Map, src []byte) *CompositeBinaryScanner { - rp := 0 - if len(src[rp:]) < 4 { - return &CompositeBinaryScanner{err: fmt.Errorf("Record incomplete %v", src)} - } + r := pgio.NewReader(src) - fieldCount := int32(binary.BigEndian.Uint32(src[rp:])) - rp += 4 + // Each field requires at least 8 bytes: 4 for the OID and 4 for the length prefix. + fieldCount := r.Count(8) + if err := r.Err(); err != nil { + return &CompositeBinaryScanner{err: fmt.Errorf("Record incomplete: %w", err)} + } return &CompositeBinaryScanner{ m: m, - rp: rp, - src: src, - fieldCount: fieldCount, + r: r, + fieldCount: int32(fieldCount), } } @@ -312,30 +313,16 @@ func (cfs *CompositeBinaryScanner) Next() bool { return false } - if cfs.rp == len(cfs.src) { + if cfs.r.Remaining() == 0 { return false } - if len(cfs.src[cfs.rp:]) < 8 { - cfs.err = fmt.Errorf("Record incomplete %v", cfs.src) + cfs.fieldOID = cfs.r.Uint32() + cfs.fieldBytes, _ = cfs.r.Value() + if err := cfs.r.Err(); err != nil { + cfs.err = fmt.Errorf("Record incomplete: %w", err) return false } - cfs.fieldOID = binary.BigEndian.Uint32(cfs.src[cfs.rp:]) - cfs.rp += 4 - - fieldLen := int(int32(binary.BigEndian.Uint32(cfs.src[cfs.rp:]))) - cfs.rp += 4 - - if fieldLen >= 0 { - if len(cfs.src[cfs.rp:]) < fieldLen { - cfs.err = fmt.Errorf("Record incomplete rp=%d src=%v", cfs.rp, cfs.src) - return false - } - cfs.fieldBytes = cfs.src[cfs.rp : cfs.rp+fieldLen] - cfs.rp += fieldLen - } else { - cfs.fieldBytes = nil - } return true } @@ -344,6 +331,17 @@ func (cfs *CompositeBinaryScanner) FieldCount() int { return int(cfs.fieldCount) } +// compositeFieldTarget returns the scan target for field i. CompositeFields is +// a slice, so indexing past its end would panic. A source with more fields than +// the destination is a mismatch that must be reported as an error. +func compositeFieldTarget(targetScanner CompositeIndexScanner, i int) (any, error) { + if cf, ok := targetScanner.(CompositeFields); ok && i >= len(cf) { + return nil, fmt.Errorf("cannot scan composite field %d into CompositeFields of length %d", i, len(cf)) + } + + return targetScanner.ScanIndex(i), nil +} + // Bytes returns the bytes of the field most recently read by Scan(). func (cfs *CompositeBinaryScanner) Bytes() []byte { return cfs.fieldBytes @@ -396,7 +394,7 @@ func (cfs *CompositeTextScanner) Next() bool { return false } - if cfs.rp == len(cfs.src) { + if cfs.rp >= len(cfs.src) { return false } @@ -410,12 +408,18 @@ func (cfs *CompositeTextScanner) Next() bool { cfs.fieldBytes = make([]byte, 0, 16) quotedValue: for { + // A quote or escape may have consumed the final ')', leaving an + // unterminated quoted field. + if cfs.rp >= len(cfs.src) { + cfs.err = fmt.Errorf("composite text format unterminated quoted field") + return false + } ch := cfs.src[cfs.rp] switch ch { case '"': cfs.rp++ - if cfs.src[cfs.rp] == '"' { + if cfs.rp < len(cfs.src) && cfs.src[cfs.rp] == '"' { cfs.fieldBytes = append(cfs.fieldBytes, '"') cfs.rp++ } else { @@ -423,6 +427,10 @@ func (cfs *CompositeTextScanner) Next() bool { } case '\\': cfs.rp++ + if cfs.rp >= len(cfs.src) { + cfs.err = fmt.Errorf("composite text format unterminated quoted field") + return false + } cfs.fieldBytes = append(cfs.fieldBytes, cfs.src[cfs.rp]) cfs.rp++ default: diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/date.go b/vendor/github.com/jackc/pgx/v5/pgtype/date.go index 305d83c..82c5e74 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/date.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/date.go @@ -2,12 +2,11 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "encoding/json" "fmt" - "strconv" "time" + "github.com/jackc/pgx/v5/internal/pgdatetime" "github.com/jackc/pgx/v5/internal/pgio" ) @@ -189,33 +188,7 @@ func (encodePlanDateCodecText) Encode(value any, buf []byte) (newBuf []byte, err switch date.InfinityModifier { case Finite: - // Year 0000 is 1 BC - bc := false - year := date.Time.Year() - if year <= 0 { - year = -year + 1 - bc = true - } - - yearBytes := strconv.AppendInt(make([]byte, 0, 6), int64(year), 10) - for i := len(yearBytes); i < 4; i++ { - buf = append(buf, '0') - } - buf = append(buf, yearBytes...) - buf = append(buf, '-') - if date.Time.Month() < 10 { - buf = append(buf, '0') - } - buf = strconv.AppendInt(buf, int64(date.Time.Month()), 10) - buf = append(buf, '-') - if date.Time.Day() < 10 { - buf = append(buf, '0') - } - buf = strconv.AppendInt(buf, int64(date.Time.Day()), 10) - - if bc { - buf = append(buf, " BC"...) - } + buf = pgdatetime.AppendDate(buf, date.Time) case Infinity: buf = append(buf, "infinity"...) case NegativeInfinity: @@ -243,17 +216,18 @@ func (DateCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan type scanPlanBinaryDateToDateScanner struct{} func (scanPlanBinaryDateToDateScanner) Scan(src []byte, dst any) error { - scanner := (dst).(DateScanner) + scanner := dst.(DateScanner) if src == nil { return scanner.ScanDate(Date{}) } - if len(src) != 4 { - return fmt.Errorf("invalid length for date: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("date: %w", err) } - dayOffset := int32(binary.BigEndian.Uint32(src)) + dayOffset := int32(raw) switch dayOffset { case infinityDayOffset: @@ -262,6 +236,10 @@ func (scanPlanBinaryDateToDateScanner) Scan(src []byte, dst any) error { return scanner.ScanDate(Date{InfinityModifier: -Infinity, Valid: true}) default: t := time.Date(2000, 1, int(1+dayOffset), 0, 0, 0, 0, time.UTC) + if t.Before(minDateTime) || !t.Before(endDate) { + return fmt.Errorf("date %d days from 2000-01-01 is out of range", dayOffset) + } + return scanner.ScanDate(Date{Time: t, Valid: true}) } } @@ -269,112 +247,33 @@ func (scanPlanBinaryDateToDateScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToDateScanner struct{} func (scanPlanTextAnyToDateScanner) Scan(src []byte, dst any) error { - scanner := (dst).(DateScanner) + scanner := dst.(DateScanner) if src == nil { return scanner.ScanDate(Date{}) } - // Check infinity cases first - if len(src) == 8 && string(src) == "infinity" { - return scanner.ScanDate(Date{InfinityModifier: Infinity, Valid: true}) - } - if len(src) == 9 && string(src) == "-infinity" { - return scanner.ScanDate(Date{InfinityModifier: -Infinity, Valid: true}) - } - - // Format: YYYY-MM-DD or YYYY...-MM-DD BC - // Minimum: 10 chars (2000-01-01), with BC: 13 chars - if len(src) < 10 { - return fmt.Errorf("invalid date format") - } - - // Check for BC suffix - bc := false - datePart := src - if len(src) >= 13 && string(src[len(src)-3:]) == " BC" { - bc = true - datePart = src[:len(src)-3] - } - - // Find year-month separator (first dash after at least 4 digits) - yearEnd := -1 - for i := 4; i < len(datePart); i++ { - if datePart[i] == '-' { - yearEnd = i - break - } - if datePart[i] < '0' || datePart[i] > '9' { - return fmt.Errorf("invalid date format") - } - } - if yearEnd == -1 || yearEnd+6 > len(datePart) { - return fmt.Errorf("invalid date format") - } - - // Validate: -MM-DD structure after year - if datePart[yearEnd+3] != '-' { - return fmt.Errorf("invalid date format") - } - - // Parse year - year, err := parseDigits(datePart[:yearEnd]) + dt, err := parseTextDateTime(src) if err != nil { - return fmt.Errorf("invalid date format") - } - - // Parse month (2 digits) - month, err := parse2Digits(datePart[yearEnd+1 : yearEnd+3]) - if err != nil { - return fmt.Errorf("invalid date format") + return err } - // Parse day (2 digits) - day, err := parse2Digits(datePart[yearEnd+4 : yearEnd+6]) - if err != nil { - return fmt.Errorf("invalid date format") + if dt.infinity != Finite { + return scanner.ScanDate(Date{InfinityModifier: dt.infinity, Valid: true}) } - // Ensure nothing extra after day - if yearEnd+6 != len(datePart) { - return fmt.Errorf("invalid date format") + if dt.hasTime { + return badDateTime(src) } - if bc { - year = -year + 1 + t, err := dt.toTime(src, "date", endDate) + if err != nil { + return err } - t := time.Date(int(year), time.Month(month), int(day), 0, 0, 0, 0, time.UTC) return scanner.ScanDate(Date{Time: t, Valid: true}) } -// parse2Digits parses exactly 2 ASCII digits. -func parse2Digits(b []byte) (int64, error) { - if len(b) != 2 { - return 0, fmt.Errorf("expected 2 digits") - } - d1, d2 := b[0], b[1] - if d1 < '0' || d1 > '9' || d2 < '0' || d2 > '9' { - return 0, fmt.Errorf("expected digits") - } - return int64(d1-'0')*10 + int64(d2-'0'), nil -} - -// parseDigits parses a sequence of ASCII digits. -func parseDigits(b []byte) (int64, error) { - if len(b) == 0 { - return 0, fmt.Errorf("empty") - } - var n int64 - for _, c := range b { - if c < '0' || c > '9' { - return 0, fmt.Errorf("non-digit") - } - n = n*10 + int64(c-'0') - } - return n, nil -} - func (c DateCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) { if src == nil { return nil, nil diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/datetime_text.go b/vendor/github.com/jackc/pgx/v5/pgtype/datetime_text.go new file mode 100644 index 0000000..6a7ff76 --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/pgtype/datetime_text.go @@ -0,0 +1,378 @@ +package pgtype + +import ( + "fmt" + "math" + "strconv" + "time" +) + +// PostgreSQL's ISO date/time output grammar, from EncodeDateTime, EncodeTimezone and +// AppendSeconds in src/backend/utils/adt/datetime.c (the USE_ISO_DATES branch): +// +// value = "infinity" / "-infinity" / stamp +// stamp = date [ SP time [ offset ] ] [ " BC" ] +// date = year "-" 2DIGIT "-" 2DIGIT +// year = 4*DIGIT ; zero-padded to exactly 4, never wider than necessary +// time = 2DIGIT ":" 2DIGIT ":" 2DIGIT [ "." 1*6DIGIT ] +// offset = ("+" / "-") 2*DIGIT [ ":" 2DIGIT [ ":" 2DIGIT ] ] +// +// " BC" is always last, after any offset. Fractional seconds are omitted entirely when +// zero and never carry trailing zeros. This is only the shape the server emits under the +// default ISO DateStyle; the other styles are not supported. +// +// Writing the format is the other half of this and lives in internal/pgdatetime, which the +// simple protocol's query sanitizer shares. + +const ( + secondsPerHour = 60 * 60 + + // maxDateTimeYearDigits is the width of the largest year PostgreSQL can represent, + // which is date's 5874897. Capping the digit count keeps the accumulated year inside + // an int on 32 bit platforms as well as 64 bit ones. + maxDateTimeYearDigits = 7 +) + +// PostgreSQL's valid ranges, from src/include/datatype/timestamp.h. They differ by type: +// date runs to 5874897 AD (JULIAN_MAXYEAR) while timestamp stops at 294276 AD +// (END_TIMESTAMP). Both share a lower bound of 4714-11-24 BC. +var ( + minDateTime = time.Date(-4713, 11, 24, 0, 0, 0, 0, time.UTC) + endDate = time.Date(5874898, 1, 1, 0, 0, 0, 0, time.UTC) + endTimestamp = time.Date(294277, 1, 1, 0, 0, 0, 0, time.UTC) +) + +// textDateTime is a date/time value parsed from PostgreSQL's text format. It holds the +// fields as written on the wire rather than a time.Time so that each codec can apply its +// own range limits and time zone rules. +type textDateTime struct { + // year uses astronomical numbering: 1 BC is 0, 2 BC is -1. + year int + month int + day int + + hour int + min int + sec int + // nsec is always a multiple of 1000. Fractional seconds are rounded to microseconds + // the way the server rounds them. + nsec int + + // offset is seconds east of UTC and is only meaningful when hasOffset is true. + offset int + + hasTime bool + hasOffset bool + + infinity InfinityModifier +} + +// in builds the parsed fields as a time.Time in loc. The fields are a wall clock reading, +// so this reinterprets them in loc rather than converting an instant into it. +func (dt textDateTime) in(loc *time.Location) time.Time { + return time.Date(dt.year, time.Month(dt.month), dt.day, dt.hour, dt.min, dt.sec, dt.nsec, loc) +} + +// toTime resolves the parsed fields to the instant they name, applying any time zone +// offset, and rejects anything outside the range of the type named by name. The upper +// limit differs by type -- date reaches 5874897 AD while timestamp stops at 294276 AD -- +// so end is supplied by the caller and is exclusive. src is only used to build the error. +func (dt textDateTime) toTime(src []byte, name string, end time.Time) (time.Time, error) { + t := dt.in(time.UTC).Add(-time.Duration(dt.offset) * time.Second) + if t.Before(minDateTime) || !t.Before(end) { + return time.Time{}, fmt.Errorf("%s %q is out of range", name, src) + } + + return t, nil +} + +// parseTextDateTime parses the grammar above. It validates the grammar and the calendar +// but applies no range limits, because PostgreSQL's limits differ by type: date reaches +// 5874897 AD while timestamp stops at 294276 AD. +func parseTextDateTime(src []byte) (textDateTime, error) { + var dt textDateTime + + switch string(src) { + case "infinity": + dt.infinity = Infinity + return dt, nil + case "-infinity": + dt.infinity = NegativeInfinity + return dt, nil + } + + s := src + bc := false + if len(s) > 3 && s[len(s)-3] == ' ' && s[len(s)-2] == 'B' && s[len(s)-1] == 'C' { + s = s[:len(s)-3] + bc = true + } + + i := 0 + + // Year. Variable width, so it runs to the first non-digit rather than a fixed offset. + yearStart := i + for i < len(s) && isDigit(s[i]) { + if i-yearStart >= maxDateTimeYearDigits { + return dt, badDateTime(src) + } + dt.year = dt.year*10 + int(s[i]-'0') + i++ + } + if i-yearStart < 4 || dt.year < 1 { + return dt, badDateTime(src) + } + + var ok bool + if i, ok = expect(s, i, '-'); !ok { + return dt, badDateTime(src) + } + if dt.month, i, ok = parse2Digits(s, i); !ok { + return dt, badDateTime(src) + } + if i, ok = expect(s, i, '-'); !ok { + return dt, badDateTime(src) + } + if dt.day, i, ok = parse2Digits(s, i); !ok { + return dt, badDateTime(src) + } + + if bc { + dt.year = 1 - dt.year + } + + if dt.month < 1 || dt.month > 12 || dt.day < 1 || dt.day > daysInMonth(dt.year, dt.month) { + return dt, badDateTime(src) + } + + // Time of day. + var carrySecond bool + if i < len(s) && s[i] == ' ' { + i++ + dt.hasTime = true + + if dt.hour, i, ok = parse2Digits(s, i); !ok { + return dt, badDateTime(src) + } + if i, ok = expect(s, i, ':'); !ok { + return dt, badDateTime(src) + } + if dt.min, i, ok = parse2Digits(s, i); !ok { + return dt, badDateTime(src) + } + if i, ok = expect(s, i, ':'); !ok { + return dt, badDateTime(src) + } + if dt.sec, i, ok = parse2Digits(s, i); !ok { + return dt, badDateTime(src) + } + if dt.hour > 23 || dt.min > 59 || dt.sec > 59 { + return dt, badDateTime(src) + } + + if i < len(s) && s[i] == '.' { + i++ + fracStart := i + for i < len(s) && isDigit(s[i]) { + i++ + } + if i == fracStart { + return dt, badDateTime(src) + } + + usec, err := roundFractionToMicroseconds(s[fracStart:i]) + if err != nil { + return dt, badDateTime(src) + } + if usec == microsecondsPerSecond { + usec = 0 + carrySecond = true + } + dt.nsec = usec * 1000 + } + + // Time zone displacement. + if i < len(s) { + switch s[i] { + case 'Z': + // Not something the server emits, but pgx's own text encoder writes it. + i++ + dt.hasOffset = true + case '+', '-': + if dt.offset, i, ok = parseOffset(s, i); !ok { + return dt, badDateTime(src) + } + dt.hasOffset = true + } + } + } + + if i != len(s) { + return dt, badDateTime(src) + } + + if carrySecond { + dt.addSecond() + } + + return dt, nil +} + +// addSecond advances by one second, carrying through to the year. It is only reached when +// rounding the fractional part rolls over, and it assumes the fields are already valid. +func (dt *textDateTime) addSecond() { + dt.sec++ + if dt.sec < 60 { + return + } + dt.sec = 0 + + dt.min++ + if dt.min < 60 { + return + } + dt.min = 0 + + dt.hour++ + if dt.hour < 24 { + return + } + dt.hour = 0 + + dt.day++ + if dt.day <= daysInMonth(dt.year, dt.month) { + return + } + dt.day = 1 + + dt.month++ + if dt.month <= 12 { + return + } + dt.month = 1 + + // Astronomical numbering has a year 0, so this needs no adjustment for the AD/BC + // boundary. + dt.year++ +} + +// roundFractionToMicroseconds converts fractional second digits to microseconds. The +// result may be microsecondsPerSecond, which the caller must carry. +func roundFractionToMicroseconds(digits []byte) (int, error) { + if len(digits) <= 6 { + usec := 0 + for _, c := range digits { + usec = usec*10 + int(c-'0') + } + for i := len(digits); i < 6; i++ { + usec *= 10 + } + return usec, nil + } + + // PostgreSQL's ParseFractionalSecond reads the fraction with strtod and rounds with + // rint, which is round-half-to-even on a binary double. Mirror that arithmetic + // exactly rather than rounding the decimal digits, so that values the server rounds + // down are not rounded up here: the server turns .0000005 into 0, while rounding the + // digits half away from zero would produce 1 microsecond. + // + // The server never emits more than six digits, so this path only handles input from + // other sources. + f, err := strconv.ParseFloat("0."+string(digits), 64) + if err != nil { + return 0, err + } + + return int(math.RoundToEven(f * 1e6)), nil +} + +func parseOffset(s []byte, i int) (offset, next int, ok bool) { + neg := s[i] == '-' + i++ + + var hour, min, sec int + if hour, i, ok = parse2Digits(s, i); !ok { + return 0, i, false + } + // PostgreSQL's MAX_TZDISP_HOUR limits numeric input, not output from named + // or POSIX time zones. These can emit offsets with more than two hour digits. + // Bound by the server's int32 seconds representation instead. + for i < len(s) && isDigit(s[i]) { + hour = hour*10 + int(s[i]-'0') + i++ + if hour > math.MaxInt32/secondsPerHour { + return 0, i, false + } + } + if i, ok = expect(s, i, ':'); ok { + if min, i, ok = parse2Digits(s, i); !ok { + return 0, i, false + } + if i, ok = expect(s, i, ':'); ok { + if sec, i, ok = parse2Digits(s, i); !ok { + return 0, i, false + } + } + } + + if min > 59 || sec > 59 { + return 0, i, false + } + + seconds := int64(hour)*secondsPerHour + int64(min*60+sec) + if neg { + seconds = -seconds + } + if seconds < math.MinInt32 || seconds > math.MaxInt32 { + return 0, i, false + } + + return int(seconds), i, true +} + +func parse2Digits(s []byte, i int) (v, next int, ok bool) { + if i+1 >= len(s) || !isDigit(s[i]) || !isDigit(s[i+1]) { + return 0, i, false + } + return int(s[i]-'0')*10 + int(s[i+1]-'0'), i + 2, true +} + +func expect(s []byte, i int, c byte) (next int, ok bool) { + if i >= len(s) || s[i] != c { + return i, false + } + return i + 1, true +} + +func isDigit(c byte) bool { return c >= '0' && c <= '9' } + +func badDateTime(src []byte) error { + const maxLen = 64 + s := string(src) + if len(s) > maxLen { + s = s[:maxLen] + "..." + } + return fmt.Errorf("invalid PostgreSQL date/time value %q", s) +} + +// isLeapYear reports whether year, in astronomical numbering, is a leap year in the +// proleptic Gregorian calendar. Go's % keeps the sign of the dividend, so the usual +// divisibility tests hold for years at or below zero. +func isLeapYear(year int) bool { + return year%4 == 0 && (year%100 != 0 || year%400 == 0) +} + +func daysInMonth(year, month int) int { + switch month { + case 1, 3, 5, 7, 8, 10, 12: + return 31 + case 4, 6, 9, 11: + return 30 + case 2: + if isLeapYear(year) { + return 29 + } + return 28 + } + return 0 +} diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/enum_codec.go b/vendor/github.com/jackc/pgx/v5/pgtype/enum_codec.go index 5e787c1..f4173ad 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/enum_codec.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/enum_codec.go @@ -88,7 +88,7 @@ func (plan *scanPlanTextAnyToEnumString) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - p := (dst).(*string) + p := dst.(*string) *p = plan.codec.lookupAndCacheString(src) return nil @@ -99,7 +99,7 @@ type scanPlanTextAnyToEnumTextScanner struct { } func (plan *scanPlanTextAnyToEnumTextScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TextScanner) + scanner := dst.(TextScanner) if src == nil { return scanner.ScanText(Text{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/float4.go b/vendor/github.com/jackc/pgx/v5/pgtype/float4.go index a43553f..8c3c1bf 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/float4.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/float4.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "encoding/json" "fmt" "math" @@ -208,12 +207,13 @@ func (scanPlanBinaryFloat4ToFloat32) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for float4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("float4: %w", err) } - n := int32(binary.BigEndian.Uint32(src)) - f := (dst).(*float32) + n := int32(raw) + f := dst.(*float32) *f = math.Float32frombits(uint32(n)) return nil @@ -222,34 +222,36 @@ func (scanPlanBinaryFloat4ToFloat32) Scan(src []byte, dst any) error { type scanPlanBinaryFloat4ToFloat64Scanner struct{} func (scanPlanBinaryFloat4ToFloat64Scanner) Scan(src []byte, dst any) error { - s := (dst).(Float64Scanner) + s := dst.(Float64Scanner) if src == nil { return s.ScanFloat64(Float8{}) } - if len(src) != 4 { - return fmt.Errorf("invalid length for float4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("float4: %w", err) } - n := int32(binary.BigEndian.Uint32(src)) + n := int32(raw) return s.ScanFloat64(Float8{Float64: float64(math.Float32frombits(uint32(n))), Valid: true}) } type scanPlanBinaryFloat4ToInt64Scanner struct{} func (scanPlanBinaryFloat4ToInt64Scanner) Scan(src []byte, dst any) error { - s := (dst).(Int64Scanner) + s := dst.(Int64Scanner) if src == nil { return s.ScanInt64(Int8{}) } - if len(src) != 4 { - return fmt.Errorf("invalid length for float4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("float4: %w", err) } - ui32 := int32(binary.BigEndian.Uint32(src)) + ui32 := int32(raw) f32 := math.Float32frombits(uint32(ui32)) i64 := int64(f32) if f32 != float32(i64) { @@ -262,17 +264,18 @@ func (scanPlanBinaryFloat4ToInt64Scanner) Scan(src []byte, dst any) error { type scanPlanBinaryFloat4ToTextScanner struct{} func (scanPlanBinaryFloat4ToTextScanner) Scan(src []byte, dst any) error { - s := (dst).(TextScanner) + s := dst.(TextScanner) if src == nil { return s.ScanText(Text{}) } - if len(src) != 4 { - return fmt.Errorf("invalid length for float4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("float4: %w", err) } - ui32 := int32(binary.BigEndian.Uint32(src)) + ui32 := int32(raw) f32 := math.Float32frombits(uint32(ui32)) return s.ScanText(Text{String: strconv.FormatFloat(float64(f32), 'f', -1, 32), Valid: true}) @@ -290,7 +293,7 @@ func (scanPlanTextAnyToFloat32) Scan(src []byte, dst any) error { return err } - f := (dst).(*float32) + f := dst.(*float32) *f = float32(n) return nil diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/float8.go b/vendor/github.com/jackc/pgx/v5/pgtype/float8.go index 6234231..ec55bd8 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/float8.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/float8.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "encoding/json" "fmt" "math" @@ -246,12 +245,13 @@ func (scanPlanBinaryFloat8ToFloat64) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for float8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("float8: %w", err) } - n := int64(binary.BigEndian.Uint64(src)) - f := (dst).(*float64) + n := int64(raw) + f := dst.(*float64) *f = math.Float64frombits(uint64(n)) return nil @@ -260,34 +260,36 @@ func (scanPlanBinaryFloat8ToFloat64) Scan(src []byte, dst any) error { type scanPlanBinaryFloat8ToFloat64Scanner struct{} func (scanPlanBinaryFloat8ToFloat64Scanner) Scan(src []byte, dst any) error { - s := (dst).(Float64Scanner) + s := dst.(Float64Scanner) if src == nil { return s.ScanFloat64(Float8{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for float8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("float8: %w", err) } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) return s.ScanFloat64(Float8{Float64: math.Float64frombits(uint64(n)), Valid: true}) } type scanPlanBinaryFloat8ToInt64Scanner struct{} func (scanPlanBinaryFloat8ToInt64Scanner) Scan(src []byte, dst any) error { - s := (dst).(Int64Scanner) + s := dst.(Int64Scanner) if src == nil { return s.ScanInt64(Int8{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for float8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("float8: %w", err) } - ui64 := int64(binary.BigEndian.Uint64(src)) + ui64 := int64(raw) f64 := math.Float64frombits(uint64(ui64)) i64 := int64(f64) if f64 != float64(i64) { @@ -300,17 +302,18 @@ func (scanPlanBinaryFloat8ToInt64Scanner) Scan(src []byte, dst any) error { type scanPlanBinaryFloat8ToTextScanner struct{} func (scanPlanBinaryFloat8ToTextScanner) Scan(src []byte, dst any) error { - s := (dst).(TextScanner) + s := dst.(TextScanner) if src == nil { return s.ScanText(Text{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for float8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("float8: %w", err) } - ui64 := int64(binary.BigEndian.Uint64(src)) + ui64 := int64(raw) f64 := math.Float64frombits(uint64(ui64)) return s.ScanText(Text{String: strconv.FormatFloat(f64, 'f', -1, 64), Valid: true}) @@ -328,7 +331,7 @@ func (scanPlanTextAnyToFloat64) Scan(src []byte, dst any) error { return err } - f := (dst).(*float64) + f := dst.(*float64) *f = n return nil @@ -337,7 +340,7 @@ func (scanPlanTextAnyToFloat64) Scan(src []byte, dst any) error { type scanPlanTextAnyToFloat64Scanner struct{} func (scanPlanTextAnyToFloat64Scanner) Scan(src []byte, dst any) error { - s := (dst).(Float64Scanner) + s := dst.(Float64Scanner) if src == nil { return s.ScanFloat64(Float8{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/hstore.go b/vendor/github.com/jackc/pgx/v5/pgtype/hstore.go index 4a2bb0a..5813768 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/hstore.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/hstore.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "errors" "fmt" "strings" @@ -180,29 +179,19 @@ func (HstoreCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPl type scanPlanBinaryHstoreToHstoreScanner struct{} func (scanPlanBinaryHstoreToHstoreScanner) Scan(src []byte, dst any) error { - scanner := (dst).(HstoreScanner) + scanner := dst.(HstoreScanner) if src == nil { return scanner.ScanHstore(Hstore(nil)) } - rp := 0 + r := pgio.NewReader(src) - const uint32Len = 4 - if len(src[rp:]) < uint32Len { - return fmt.Errorf("hstore incomplete %v", src) - } - pairCount := int(int32(binary.BigEndian.Uint32(src[rp:]))) - rp += uint32Len - - if pairCount < 0 { - return fmt.Errorf("hstore invalid pair count: %d", pairCount) - } - // Each pair carries at minimum two int32 length headers (key, value), so pairCount cannot - // exceed the remaining bytes / 8. This bounds the up-front make() against a malicious server - // claiming a huge pair count in a small message. - if maxPairs := len(src[rp:]) / (2 * uint32Len); pairCount > maxPairs { - return fmt.Errorf("hstore invalid pair count %d for %d remaining bytes", pairCount, len(src[rp:])) + // Each pair carries at minimum two int32 length headers (key, value). This bounds the + // up-front make() against a malicious server claiming a huge pair count in a small message. + pairCount := r.Count(8) + if err := r.Err(); err != nil { + return fmt.Errorf("hstore: %w", err) } hstore := make(Hstore, pairCount) @@ -210,47 +199,34 @@ func (scanPlanBinaryHstoreToHstoreScanner) Scan(src []byte, dst any) error { valueStrings := make([]string, pairCount) for i := range pairCount { - if len(src[rp:]) < uint32Len { - return fmt.Errorf("hstore incomplete %v", src) - } - keyLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) - rp += uint32Len - - if keyLen < 0 { - return fmt.Errorf("hstore invalid key length: %d", keyLen) - } - if len(src[rp:]) < keyLen { - return fmt.Errorf("hstore incomplete %v", src) + keyBytes, keyNull := r.Value() + valueBytes, valueNull := r.Value() + if err := r.Err(); err != nil { + return fmt.Errorf("hstore pair %d: %w", i, err) } - key := string(src[rp : rp+keyLen]) - rp += keyLen - - if len(src[rp:]) < uint32Len { - return fmt.Errorf("hstore incomplete %v", src) + if keyNull { + return fmt.Errorf("hstore pair %d: key cannot be NULL", i) } - valueLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) - rp += 4 - if valueLen >= 0 { - if len(src[rp:]) < valueLen { - return fmt.Errorf("hstore incomplete %v", src) - } - valueStrings[i] = string(src[rp : rp+valueLen]) - rp += valueLen - - hstore[key] = &valueStrings[i] - } else { + key := string(keyBytes) + if valueNull { hstore[key] = nil + } else { + valueStrings[i] = string(valueBytes) + hstore[key] = &valueStrings[i] } } + if err := r.Finish(); err != nil { + return fmt.Errorf("hstore: %w", err) + } return scanner.ScanHstore(hstore) } type scanPlanTextAnyToHstoreScanner struct{} func (s scanPlanTextAnyToHstoreScanner) Scan(src []byte, dst any) error { - scanner := (dst).(HstoreScanner) + scanner := dst.(HstoreScanner) if src == nil { return scanner.ScanHstore(Hstore(nil)) @@ -455,8 +431,13 @@ func parseHstore(s string) (Hstore, error) { p := newHSP(s) // This is an over-estimate of the number of key/value pairs. Use '>' because I am guessing it - // is less likely to occur in keys/values than '=' or ','. + // is less likely to occur in keys/values than '=' or ','. Clamp so an unvalidated + // separator count cannot pre-size a huge map from garbage input. + const maxHstorePairsEstimate = 1024 numPairsEstimate := strings.Count(s, ">") + if numPairsEstimate > maxHstorePairsEstimate { + numPairsEstimate = maxHstorePairsEstimate + } // makes one allocation of strings for the entire Hstore, rather than one allocation per value. valueStrings := make([]string, 0, numPairsEstimate) result := make(Hstore, numPairsEstimate) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/inet.go b/vendor/github.com/jackc/pgx/v5/pgtype/inet.go index 2592a5b..23155fe 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/inet.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/inet.go @@ -146,7 +146,7 @@ func (c InetCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (an type scanPlanBinaryInetToNetipPrefixScanner struct{} func (scanPlanBinaryInetToNetipPrefixScanner) Scan(src []byte, dst any) error { - scanner := (dst).(NetipPrefixScanner) + scanner := dst.(NetipPrefixScanner) if src == nil { return scanner.ScanNetipPrefix(netip.Prefix{}) @@ -172,7 +172,7 @@ func (scanPlanBinaryInetToNetipPrefixScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToNetipPrefixScanner struct{} func (scanPlanTextAnyToNetipPrefixScanner) Scan(src []byte, dst any) error { - scanner := (dst).(NetipPrefixScanner) + scanner := dst.(NetipPrefixScanner) if src == nil { return scanner.ScanNetipPrefix(netip.Prefix{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/int.go b/vendor/github.com/jackc/pgx/v5/pgtype/int.go index 95032e5..ddcfe54 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/int.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/int.go @@ -4,7 +4,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "encoding/json" "fmt" "math" @@ -303,8 +302,9 @@ func (scanPlanBinaryInt2ToInt8) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for int2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("int2: %w", err) } p, ok := (dst).(*int8) @@ -312,7 +312,7 @@ func (scanPlanBinaryInt2ToInt8) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int16(binary.BigEndian.Uint16(src)) + n := int16(raw) if n < math.MinInt8 { return fmt.Errorf("%d is less than minimum value for int8", n) } else if n > math.MaxInt8 { @@ -331,8 +331,9 @@ func (scanPlanBinaryInt2ToUint8) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for uint2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("uint2: %w", err) } p, ok := (dst).(*uint8) @@ -340,7 +341,7 @@ func (scanPlanBinaryInt2ToUint8) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int16(binary.BigEndian.Uint16(src)) + n := int16(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint8", n) } @@ -361,8 +362,9 @@ func (scanPlanBinaryInt2ToInt16) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for int2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("int2: %w", err) } p, ok := (dst).(*int16) @@ -370,7 +372,7 @@ func (scanPlanBinaryInt2ToInt16) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - *p = int16(binary.BigEndian.Uint16(src)) + *p = int16(raw) return nil } @@ -382,8 +384,9 @@ func (scanPlanBinaryInt2ToUint16) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for uint2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("uint2: %w", err) } p, ok := (dst).(*uint16) @@ -391,7 +394,7 @@ func (scanPlanBinaryInt2ToUint16) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int16(binary.BigEndian.Uint16(src)) + n := int16(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint16", n) } @@ -408,8 +411,9 @@ func (scanPlanBinaryInt2ToInt32) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for int2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("int2: %w", err) } p, ok := (dst).(*int32) @@ -417,7 +421,7 @@ func (scanPlanBinaryInt2ToInt32) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - *p = int32(int16(binary.BigEndian.Uint16(src))) + *p = int32(int16(raw)) return nil } @@ -429,8 +433,9 @@ func (scanPlanBinaryInt2ToUint32) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for uint2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("uint2: %w", err) } p, ok := (dst).(*uint32) @@ -438,7 +443,7 @@ func (scanPlanBinaryInt2ToUint32) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int16(binary.BigEndian.Uint16(src)) + n := int16(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint32", n) } @@ -455,8 +460,9 @@ func (scanPlanBinaryInt2ToInt64) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for int2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("int2: %w", err) } p, ok := (dst).(*int64) @@ -464,7 +470,7 @@ func (scanPlanBinaryInt2ToInt64) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - *p = int64(int16(binary.BigEndian.Uint16(src))) + *p = int64(int16(raw)) return nil } @@ -476,8 +482,9 @@ func (scanPlanBinaryInt2ToUint64) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for uint2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("uint2: %w", err) } p, ok := (dst).(*uint64) @@ -485,7 +492,7 @@ func (scanPlanBinaryInt2ToUint64) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int16(binary.BigEndian.Uint16(src)) + n := int16(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint64", n) } @@ -502,8 +509,9 @@ func (scanPlanBinaryInt2ToInt) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for int2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("int2: %w", err) } p, ok := (dst).(*int) @@ -511,7 +519,7 @@ func (scanPlanBinaryInt2ToInt) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - *p = int(int16(binary.BigEndian.Uint16(src))) + *p = int(int16(raw)) return nil } @@ -523,8 +531,9 @@ func (scanPlanBinaryInt2ToUint) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 2 { - return fmt.Errorf("invalid length for uint2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("uint2: %w", err) } p, ok := (dst).(*uint) @@ -532,7 +541,7 @@ func (scanPlanBinaryInt2ToUint) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(int16(binary.BigEndian.Uint16(src))) + n := int64(int16(raw)) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint", n) } @@ -554,11 +563,12 @@ func (scanPlanBinaryInt2ToInt64Scanner) Scan(src []byte, dst any) error { return s.ScanInt64(Int8{}) } - if len(src) != 2 { - return fmt.Errorf("invalid length for int2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("int2: %w", err) } - n := int64(int16(binary.BigEndian.Uint16(src))) + n := int64(int16(raw)) return s.ScanInt64(Int8{Int64: n, Valid: true}) } @@ -575,11 +585,12 @@ func (scanPlanBinaryInt2ToTextScanner) Scan(src []byte, dst any) error { return s.ScanText(Text{}) } - if len(src) != 2 { - return fmt.Errorf("invalid length for int2: %v", len(src)) + raw, err := pgio.Uint16Exact(src) + if err != nil { + return fmt.Errorf("int2: %w", err) } - n := int64(int16(binary.BigEndian.Uint16(src))) + n := int64(int16(raw)) return s.ScanText(Text{String: strconv.FormatInt(n, 10), Valid: true}) } @@ -866,8 +877,9 @@ func (scanPlanBinaryInt4ToInt8) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for int4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("int4: %w", err) } p, ok := (dst).(*int8) @@ -875,7 +887,7 @@ func (scanPlanBinaryInt4ToInt8) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int32(binary.BigEndian.Uint32(src)) + n := int32(raw) if n < math.MinInt8 { return fmt.Errorf("%d is less than minimum value for int8", n) } else if n > math.MaxInt8 { @@ -894,8 +906,9 @@ func (scanPlanBinaryInt4ToUint8) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for uint4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("uint4: %w", err) } p, ok := (dst).(*uint8) @@ -903,7 +916,7 @@ func (scanPlanBinaryInt4ToUint8) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int32(binary.BigEndian.Uint32(src)) + n := int32(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint8", n) } @@ -924,8 +937,9 @@ func (scanPlanBinaryInt4ToInt16) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for int4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("int4: %w", err) } p, ok := (dst).(*int16) @@ -933,7 +947,7 @@ func (scanPlanBinaryInt4ToInt16) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int32(binary.BigEndian.Uint32(src)) + n := int32(raw) if n < math.MinInt16 { return fmt.Errorf("%d is less than minimum value for int16", n) } else if n > math.MaxInt16 { @@ -952,8 +966,9 @@ func (scanPlanBinaryInt4ToUint16) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for uint4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("uint4: %w", err) } p, ok := (dst).(*uint16) @@ -961,7 +976,7 @@ func (scanPlanBinaryInt4ToUint16) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int32(binary.BigEndian.Uint32(src)) + n := int32(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint16", n) } @@ -982,8 +997,9 @@ func (scanPlanBinaryInt4ToInt32) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for int4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("int4: %w", err) } p, ok := (dst).(*int32) @@ -991,7 +1007,7 @@ func (scanPlanBinaryInt4ToInt32) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - *p = int32(binary.BigEndian.Uint32(src)) + *p = int32(raw) return nil } @@ -1003,8 +1019,9 @@ func (scanPlanBinaryInt4ToUint32) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for uint4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("uint4: %w", err) } p, ok := (dst).(*uint32) @@ -1012,7 +1029,7 @@ func (scanPlanBinaryInt4ToUint32) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int32(binary.BigEndian.Uint32(src)) + n := int32(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint32", n) } @@ -1029,8 +1046,9 @@ func (scanPlanBinaryInt4ToInt64) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for int4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("int4: %w", err) } p, ok := (dst).(*int64) @@ -1038,7 +1056,7 @@ func (scanPlanBinaryInt4ToInt64) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - *p = int64(int32(binary.BigEndian.Uint32(src))) + *p = int64(int32(raw)) return nil } @@ -1050,8 +1068,9 @@ func (scanPlanBinaryInt4ToUint64) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for uint4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("uint4: %w", err) } p, ok := (dst).(*uint64) @@ -1059,7 +1078,7 @@ func (scanPlanBinaryInt4ToUint64) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int32(binary.BigEndian.Uint32(src)) + n := int32(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint64", n) } @@ -1076,8 +1095,9 @@ func (scanPlanBinaryInt4ToInt) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for int4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("int4: %w", err) } p, ok := (dst).(*int) @@ -1085,7 +1105,7 @@ func (scanPlanBinaryInt4ToInt) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - *p = int(int32(binary.BigEndian.Uint32(src))) + *p = int(int32(raw)) return nil } @@ -1097,8 +1117,9 @@ func (scanPlanBinaryInt4ToUint) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for uint4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("uint4: %w", err) } p, ok := (dst).(*uint) @@ -1106,7 +1127,7 @@ func (scanPlanBinaryInt4ToUint) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(int32(binary.BigEndian.Uint32(src))) + n := int64(int32(raw)) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint", n) } @@ -1128,11 +1149,12 @@ func (scanPlanBinaryInt4ToInt64Scanner) Scan(src []byte, dst any) error { return s.ScanInt64(Int8{}) } - if len(src) != 4 { - return fmt.Errorf("invalid length for int4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("int4: %w", err) } - n := int64(int32(binary.BigEndian.Uint32(src))) + n := int64(int32(raw)) return s.ScanInt64(Int8{Int64: n, Valid: true}) } @@ -1149,11 +1171,12 @@ func (scanPlanBinaryInt4ToTextScanner) Scan(src []byte, dst any) error { return s.ScanText(Text{}) } - if len(src) != 4 { - return fmt.Errorf("invalid length for int4: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("int4: %w", err) } - n := int64(int32(binary.BigEndian.Uint32(src))) + n := int64(int32(raw)) return s.ScanText(Text{String: strconv.FormatInt(n, 10), Valid: true}) } @@ -1215,7 +1238,7 @@ func (dst *Int8) Scan(src any) error { } if n < math.MinInt64 { - return fmt.Errorf("%d is greater than maximum value for Int8", n) + return fmt.Errorf("%d is less than minimum value for Int8", n) } if n > math.MaxInt64 { return fmt.Errorf("%d is greater than maximum value for Int8", n) @@ -1440,8 +1463,9 @@ func (scanPlanBinaryInt8ToInt8) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for int8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("int8: %w", err) } p, ok := (dst).(*int8) @@ -1449,7 +1473,7 @@ func (scanPlanBinaryInt8ToInt8) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) if n < math.MinInt8 { return fmt.Errorf("%d is less than minimum value for int8", n) } else if n > math.MaxInt8 { @@ -1468,8 +1492,9 @@ func (scanPlanBinaryInt8ToUint8) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for uint8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("uint8: %w", err) } p, ok := (dst).(*uint8) @@ -1477,7 +1502,7 @@ func (scanPlanBinaryInt8ToUint8) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint8", n) } @@ -1498,8 +1523,9 @@ func (scanPlanBinaryInt8ToInt16) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for int8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("int8: %w", err) } p, ok := (dst).(*int16) @@ -1507,7 +1533,7 @@ func (scanPlanBinaryInt8ToInt16) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) if n < math.MinInt16 { return fmt.Errorf("%d is less than minimum value for int16", n) } else if n > math.MaxInt16 { @@ -1526,8 +1552,9 @@ func (scanPlanBinaryInt8ToUint16) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for uint8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("uint8: %w", err) } p, ok := (dst).(*uint16) @@ -1535,7 +1562,7 @@ func (scanPlanBinaryInt8ToUint16) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint16", n) } @@ -1556,8 +1583,9 @@ func (scanPlanBinaryInt8ToInt32) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for int8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("int8: %w", err) } p, ok := (dst).(*int32) @@ -1565,7 +1593,7 @@ func (scanPlanBinaryInt8ToInt32) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) if n < math.MinInt32 { return fmt.Errorf("%d is less than minimum value for int32", n) } else if n > math.MaxInt32 { @@ -1584,8 +1612,9 @@ func (scanPlanBinaryInt8ToUint32) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for uint8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("uint8: %w", err) } p, ok := (dst).(*uint32) @@ -1593,7 +1622,7 @@ func (scanPlanBinaryInt8ToUint32) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint32", n) } @@ -1614,8 +1643,9 @@ func (scanPlanBinaryInt8ToInt64) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for int8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("int8: %w", err) } p, ok := (dst).(*int64) @@ -1623,7 +1653,7 @@ func (scanPlanBinaryInt8ToInt64) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - *p = int64(binary.BigEndian.Uint64(src)) + *p = int64(raw) return nil } @@ -1635,8 +1665,9 @@ func (scanPlanBinaryInt8ToUint64) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for uint8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("uint8: %w", err) } p, ok := (dst).(*uint64) @@ -1644,7 +1675,7 @@ func (scanPlanBinaryInt8ToUint64) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint64", n) } @@ -1661,8 +1692,9 @@ func (scanPlanBinaryInt8ToInt) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for int8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("int8: %w", err) } p, ok := (dst).(*int) @@ -1670,7 +1702,7 @@ func (scanPlanBinaryInt8ToInt) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(binary.BigEndian.Uint64(src)) + n := int64(raw) if n < math.MinInt { return fmt.Errorf("%d is less than minimum value for int", n) } else if n > math.MaxInt { @@ -1689,8 +1721,9 @@ func (scanPlanBinaryInt8ToUint) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for uint8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("uint8: %w", err) } p, ok := (dst).(*uint) @@ -1698,7 +1731,7 @@ func (scanPlanBinaryInt8ToUint) Scan(src []byte, dst any) error { return ErrScanTargetTypeChanged } - n := int64(int64(binary.BigEndian.Uint64(src))) + n := int64(int64(raw)) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint", n) } @@ -1724,11 +1757,12 @@ func (scanPlanBinaryInt8ToInt64Scanner) Scan(src []byte, dst any) error { return s.ScanInt64(Int8{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for int8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("int8: %w", err) } - n := int64(int64(binary.BigEndian.Uint64(src))) + n := int64(int64(raw)) return s.ScanInt64(Int8{Int64: n, Valid: true}) } @@ -1745,11 +1779,12 @@ func (scanPlanBinaryInt8ToTextScanner) Scan(src []byte, dst any) error { return s.ScanText(Text{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for int8: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("int8: %w", err) } - n := int64(int64(binary.BigEndian.Uint64(src))) + n := int64(int64(raw)) return s.ScanText(Text{String: strconv.FormatInt(n, 10), Valid: true}) } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/int.go.erb b/vendor/github.com/jackc/pgx/v5/pgtype/int.go.erb index c2d40f6..4be6d93 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/int.go.erb +++ b/vendor/github.com/jackc/pgx/v5/pgtype/int.go.erb @@ -79,7 +79,7 @@ func (dst *Int<%= pg_byte_size %>) Scan(src any) error { } if n < math.MinInt<%= pg_bit_size %> { - return fmt.Errorf("%d is greater than maximum value for Int<%= pg_byte_size %>", n) + return fmt.Errorf("%d is less than minimum value for Int<%= pg_byte_size %>", n) } if n > math.MaxInt<%= pg_bit_size %> { return fmt.Errorf("%d is greater than maximum value for Int<%= pg_byte_size %>", n) @@ -306,8 +306,9 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToInt<%= dst_bit_size %>) Scan(src []b return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != <%= pg_byte_size %> { - return fmt.Errorf("invalid length for int<%= pg_byte_size %>: %v", len(src)) + raw, err := pgio.Uint<%= pg_bit_size %>Exact(src) + if err != nil { + return fmt.Errorf("int<%= pg_byte_size %>: %w", err) } p, ok := (dst).(*int<%= dst_bit_size %>) @@ -316,7 +317,7 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToInt<%= dst_bit_size %>) Scan(src []b } <% if dst_bit_size < pg_bit_size %> - n := int<%= pg_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src)) + n := int<%= pg_bit_size %>(raw) if n < math.MinInt<%= dst_bit_size %> { return fmt.Errorf("%d is less than minimum value for int<%= dst_bit_size %>", n) } else if n > math.MaxInt<%= dst_bit_size %> { @@ -325,9 +326,9 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToInt<%= dst_bit_size %>) Scan(src []b *p = int<%= dst_bit_size %>(n) <% elsif dst_bit_size == pg_bit_size %> - *p = int<%= dst_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src)) + *p = int<%= dst_bit_size %>(raw) <% else %> - *p = int<%= dst_bit_size %>(int<%= pg_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src))) + *p = int<%= dst_bit_size %>(int<%= pg_bit_size %>(raw)) <% end %> return nil @@ -340,8 +341,9 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToUint<%= dst_bit_size %>) Scan(src [] return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != <%= pg_byte_size %> { - return fmt.Errorf("invalid length for uint<%= pg_byte_size %>: %v", len(src)) + raw, err := pgio.Uint<%= pg_bit_size %>Exact(src) + if err != nil { + return fmt.Errorf("uint<%= pg_byte_size %>: %w", err) } p, ok := (dst).(*uint<%= dst_bit_size %>) @@ -349,7 +351,7 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToUint<%= dst_bit_size %>) Scan(src [] return ErrScanTargetTypeChanged } - n := int<%= pg_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src)) + n := int<%= pg_bit_size %>(raw) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint<%= dst_bit_size %>", n) } @@ -372,8 +374,9 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToInt) Scan(src []byte, dst any) error return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != <%= pg_byte_size %> { - return fmt.Errorf("invalid length for int<%= pg_byte_size %>: %v", len(src)) + raw, err := pgio.Uint<%= pg_bit_size %>Exact(src) + if err != nil { + return fmt.Errorf("int<%= pg_byte_size %>: %w", err) } p, ok := (dst).(*int) @@ -382,7 +385,7 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToInt) Scan(src []byte, dst any) error } <% if 32 < pg_bit_size %> - n := int64(binary.BigEndian.Uint<%= pg_bit_size %>(src)) + n := int64(raw) if n < math.MinInt { return fmt.Errorf("%d is less than minimum value for int", n) } else if n > math.MaxInt { @@ -391,7 +394,7 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToInt) Scan(src []byte, dst any) error *p = int(n) <% else %> - *p = int(int<%= pg_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src))) + *p = int(int<%= pg_bit_size %>(raw)) <% end %> return nil @@ -404,8 +407,9 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToUint) Scan(src []byte, dst any) erro return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != <%= pg_byte_size %> { - return fmt.Errorf("invalid length for uint<%= pg_byte_size %>: %v", len(src)) + raw, err := pgio.Uint<%= pg_bit_size %>Exact(src) + if err != nil { + return fmt.Errorf("uint<%= pg_byte_size %>: %w", err) } p, ok := (dst).(*uint) @@ -413,7 +417,7 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToUint) Scan(src []byte, dst any) erro return ErrScanTargetTypeChanged } - n := int64(int<%= pg_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src))) + n := int64(int<%= pg_bit_size %>(raw)) if n < 0 { return fmt.Errorf("%d is less than minimum value for uint", n) } @@ -440,12 +444,13 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToInt64Scanner) Scan(src []byte, dst a return s.ScanInt64(Int8{}) } - if len(src) != <%= pg_byte_size %> { - return fmt.Errorf("invalid length for int<%= pg_byte_size %>: %v", len(src)) + raw, err := pgio.Uint<%= pg_bit_size %>Exact(src) + if err != nil { + return fmt.Errorf("int<%= pg_byte_size %>: %w", err) } - n := int64(int<%= pg_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src))) + n := int64(int<%= pg_bit_size %>(raw)) return s.ScanInt64(Int8{Int64: n, Valid: true}) } @@ -463,12 +468,13 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToTextScanner) Scan(src []byte, dst an return s.ScanText(Text{}) } - if len(src) != <%= pg_byte_size %> { - return fmt.Errorf("invalid length for int<%= pg_byte_size %>: %v", len(src)) + raw, err := pgio.Uint<%= pg_bit_size %>Exact(src) + if err != nil { + return fmt.Errorf("int<%= pg_byte_size %>: %w", err) } - n := int64(int<%= pg_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src))) + n := int64(int<%= pg_bit_size %>(raw)) return s.ScanText(Text{String: strconv.FormatInt(n, 10), Valid: true}) } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/interval.go b/vendor/github.com/jackc/pgx/v5/pgtype/interval.go index be8decd..51a4381 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/interval.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/interval.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "strconv" "strings" @@ -175,19 +174,21 @@ func (IntervalCodec) PlanScan(m *Map, oid uint32, format int16, target any) Scan type scanPlanBinaryIntervalToIntervalScanner struct{} func (scanPlanBinaryIntervalToIntervalScanner) Scan(src []byte, dst any) error { - scanner := (dst).(IntervalScanner) + scanner := dst.(IntervalScanner) if src == nil { return scanner.ScanInterval(Interval{}) } - if len(src) != 16 { - return fmt.Errorf("Received an invalid size for an interval: %d", len(src)) - } + r := pgio.NewReader(src) + + microseconds := r.Int64() + days := r.Int32() + months := r.Int32() - microseconds := int64(binary.BigEndian.Uint64(src)) - days := int32(binary.BigEndian.Uint32(src[8:])) - months := int32(binary.BigEndian.Uint32(src[12:])) + if err := r.Finish(); err != nil { + return fmt.Errorf("Received an invalid size for an interval: %w", err) + } return scanner.ScanInterval(Interval{Microseconds: microseconds, Days: days, Months: months, Valid: true}) } @@ -195,7 +196,7 @@ func (scanPlanBinaryIntervalToIntervalScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToIntervalScanner struct{} func (scanPlanTextAnyToIntervalScanner) Scan(src []byte, dst any) error { - scanner := (dst).(IntervalScanner) + scanner := dst.(IntervalScanner) if src == nil { return scanner.ScanInterval(Interval{}) @@ -232,7 +233,7 @@ func (scanPlanTextAnyToIntervalScanner) Scan(src []byte, dst any) error { } var negative bool - if timeParts[0][0] == '-' { + if len(timeParts[0]) > 0 && timeParts[0][0] == '-' { negative = true timeParts[0] = timeParts[0][1:] } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/line.go b/vendor/github.com/jackc/pgx/v5/pgtype/line.go index 73b0636..8fb02bb 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/line.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/line.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "math" "strconv" @@ -147,19 +146,21 @@ func (LineCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan type scanPlanBinaryLineToLineScanner struct{} func (scanPlanBinaryLineToLineScanner) Scan(src []byte, dst any) error { - scanner := (dst).(LineScanner) + scanner := dst.(LineScanner) if src == nil { return scanner.ScanLine(Line{}) } - if len(src) != 24 { - return fmt.Errorf("invalid length for line: %v", len(src)) - } + r := pgio.NewReader(src) - a := binary.BigEndian.Uint64(src) - b := binary.BigEndian.Uint64(src[8:]) - c := binary.BigEndian.Uint64(src[16:]) + a := r.Uint64() + b := r.Uint64() + c := r.Uint64() + + if err := r.Finish(); err != nil { + return fmt.Errorf("line: %w", err) + } return scanner.ScanLine(Line{ A: math.Float64frombits(a), @@ -172,7 +173,7 @@ func (scanPlanBinaryLineToLineScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToLineScanner struct{} func (scanPlanTextAnyToLineScanner) Scan(src []byte, dst any) error { - scanner := (dst).(LineScanner) + scanner := dst.(LineScanner) if src == nil { return scanner.ScanLine(Line{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/lseg.go b/vendor/github.com/jackc/pgx/v5/pgtype/lseg.go index 438b45b..a239c09 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/lseg.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/lseg.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "math" "strconv" @@ -145,20 +144,22 @@ func (LsegCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan type scanPlanBinaryLsegToLsegScanner struct{} func (scanPlanBinaryLsegToLsegScanner) Scan(src []byte, dst any) error { - scanner := (dst).(LsegScanner) + scanner := dst.(LsegScanner) if src == nil { return scanner.ScanLseg(Lseg{}) } - if len(src) != 32 { - return fmt.Errorf("invalid length for lseg: %v", len(src)) - } + r := pgio.NewReader(src) - x1 := binary.BigEndian.Uint64(src) - y1 := binary.BigEndian.Uint64(src[8:]) - x2 := binary.BigEndian.Uint64(src[16:]) - y2 := binary.BigEndian.Uint64(src[24:]) + x1 := r.Uint64() + y1 := r.Uint64() + x2 := r.Uint64() + y2 := r.Uint64() + + if err := r.Finish(); err != nil { + return fmt.Errorf("lseg: %w", err) + } return scanner.ScanLseg(Lseg{ P: [2]Vec2{ @@ -172,7 +173,7 @@ func (scanPlanBinaryLsegToLsegScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToLsegScanner struct{} func (scanPlanTextAnyToLsegScanner) Scan(src []byte, dst any) error { - scanner := (dst).(LsegScanner) + scanner := dst.(LsegScanner) if src == nil { return scanner.ScanLseg(Lseg{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/ltree.go b/vendor/github.com/jackc/pgx/v5/pgtype/ltree.go index 6af3177..7607060 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/ltree.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/ltree.go @@ -21,7 +21,7 @@ func (l LtreeCodec) PreferredFormat() int16 { func (l LtreeCodec) PlanEncode(m *Map, oid uint32, format int16, value any) EncodePlan { switch format { case TextFormatCode: - return (TextCodec)(l).PlanEncode(m, oid, format, value) + return TextCodec(l).PlanEncode(m, oid, format, value) case BinaryFormatCode: switch value.(type) { case string: @@ -72,7 +72,7 @@ func (encodeLtreeCodecBinaryTextValuer) Encode(value any, buf []byte) (newBuf [] func (l LtreeCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan { switch format { case TextFormatCode: - return (TextCodec)(l).PlanScan(m, oid, format, target) + return TextCodec(l).PlanScan(m, oid, format, target) case BinaryFormatCode: switch target.(type) { case *string: @@ -93,7 +93,7 @@ func (scanPlanBinaryLtreeToString) Scan(src []byte, target any) error { return fmt.Errorf("unsupported ltree version %d", version) } - p := (target).(*string) + p := target.(*string) *p = string(src[1:]) return nil @@ -107,16 +107,16 @@ func (scanPlanBinaryLtreeToTextScanner) Scan(src []byte, target any) error { return fmt.Errorf("unsupported ltree version %d", version) } - scanner := (target).(TextScanner) + scanner := target.(TextScanner) return scanner.ScanText(Text{String: string(src[1:]), Valid: true}) } // DecodeDatabaseSQLValue returns src decoded into a value compatible with the sql.Scanner interface. func (l LtreeCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) { - return (TextCodec)(l).DecodeDatabaseSQLValue(m, oid, format, src) + return TextCodec(l).DecodeDatabaseSQLValue(m, oid, format, src) } // DecodeValue returns src decoded into its default format. func (l LtreeCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) { - return (TextCodec)(l).DecodeValue(m, oid, format, src) + return TextCodec(l).DecodeValue(m, oid, format, src) } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/macaddr.go b/vendor/github.com/jackc/pgx/v5/pgtype/macaddr.go index e913ec9..045975e 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/macaddr.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/macaddr.go @@ -116,7 +116,7 @@ func (scanPlanBinaryMacaddrToHardwareAddr) Scan(src []byte, dst any) error { type scanPlanBinaryMacaddrToTextScanner struct{} func (scanPlanBinaryMacaddrToTextScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TextScanner) + scanner := dst.(TextScanner) if src == nil { return scanner.ScanText(Text{}) } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/multirange.go b/vendor/github.com/jackc/pgx/v5/pgtype/multirange.go index 11f30b4..f47091c 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/multirange.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/multirange.go @@ -3,7 +3,6 @@ package pgtype import ( "bytes" "database/sql/driver" - "encoding/binary" "fmt" "reflect" @@ -205,14 +204,12 @@ func (c *MultirangeCodec) PlanScan(m *Map, oid uint32, format int16, target any) } func (c *MultirangeCodec) decodeBinary(m *Map, multirangeOID uint32, src []byte, multirange MultirangeSetter) error { - rp := 0 - - elementCount := int(binary.BigEndian.Uint32(src[rp:])) - rp += 4 + r := pgio.NewReader(src) // Each element requires at least 4 bytes for its length prefix. - if elementCount > len(src)/4 { - return fmt.Errorf("multirange element count %d exceeds available data", elementCount) + elementCount := r.Count(4) + if err := r.Err(); err != nil { + return fmt.Errorf("multirange: %w", err) } err := multirange.SetLen(elementCount) @@ -221,7 +218,7 @@ func (c *MultirangeCodec) decodeBinary(m *Map, multirangeOID uint32, src []byte, } if elementCount == 0 { - return nil + return r.Finish() } elementScanPlan := c.ElementType.Codec.PlanScan(m, c.ElementType.OID, BinaryFormatCode, multirange.ScanIndex(0)) @@ -231,18 +228,9 @@ func (c *MultirangeCodec) decodeBinary(m *Map, multirangeOID uint32, src []byte, for i := range elementCount { elem := multirange.ScanIndex(i) - if len(src[rp:]) < 4 { - return fmt.Errorf("multirange body truncated at element %d", i) - } - elemLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) - rp += 4 - var elemSrc []byte - if elemLen >= 0 { - if len(src[rp:]) < elemLen { - return fmt.Errorf("multirange element %d length %d exceeds remaining %d bytes", i, elemLen, len(src[rp:])) - } - elemSrc = src[rp : rp+elemLen] - rp += elemLen + elemSrc, _ := r.Value() + if err := r.Err(); err != nil { + return fmt.Errorf("multirange element %d: %w", i, err) } err = elementScanPlan.Scan(elemSrc, elem) if err != nil { @@ -250,7 +238,7 @@ func (c *MultirangeCodec) decodeBinary(m *Map, multirangeOID uint32, src []byte, } } - return nil + return r.Finish() } func (c *MultirangeCodec) decodeText(m *Map, multirangeOID uint32, src []byte, multirange MultirangeSetter) error { diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/numeric.go b/vendor/github.com/jackc/pgx/v5/pgtype/numeric.go index caf5ff1..15ad6e4 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/numeric.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/numeric.go @@ -3,7 +3,7 @@ package pgtype import ( "bytes" "database/sql/driver" - "encoding/binary" + "errors" "fmt" "math" "math/big" @@ -16,6 +16,14 @@ import ( // PostgreSQL internal numeric storage uses 16-bit "digits" with base of 10,000 const nbase = 10_000 +// Numeric's binary representation stores the exponent through a base-10,000 +// weight and an int16 dscale. These are also the largest exponents that can be +// safely materialized by the text encoders. +const ( + minNumericExponent = -math.MaxInt16 + maxNumericExponent = math.MaxInt16*4 + 3 +) + const ( pgNumericNaN = 0x00000000c0000000 pgNumericNaNSign = 0xc000 @@ -133,12 +141,7 @@ func (n *Numeric) ScanScientific(src string) error { return scanPlanTextAnyToNumericScanner{}.Scan([]byte(src), n) } - if bigF, ok := new(big.Float).SetString(src); ok { - smallF, _ := bigF.Float64() - src = strconv.FormatFloat(smallF, 'f', -1, 64) - } - - num, exp, err := parseNumericString(src) + num, exp, err := parseScientificNumericString(src) if err != nil { return err } @@ -148,7 +151,45 @@ func (n *Numeric) ScanScientific(src string) error { return nil } +func parseScientificNumericString(str string) (n *big.Int, exp int32, err error) { + idx := strings.IndexAny(str, "eE") + if idx == -1 { + return parseNumericString(str) + } + + mantissa := str[:idx] + scientificExp, err := strconv.ParseInt(str[idx+1:], 10, 32) + if err != nil { + if errors.Is(err, strconv.ErrRange) { + return nil, 0, fmt.Errorf("%s exponent out of range", str) + } + return nil, 0, fmt.Errorf("%s is not a number", str) + } + + num, mantissaExp, err := parseNumericString(mantissa) + if err != nil { + return nil, 0, fmt.Errorf("%s is not a number", str) + } + + combinedExp := int64(mantissaExp) + scientificExp + if combinedExp < minNumericExponent || combinedExp > maxNumericExponent { + return nil, 0, fmt.Errorf("%s exponent out of range", str) + } + + return num, int32(combinedExp), nil +} + func (n *Numeric) toBigInt() (*big.Int, error) { + if n.NaN { + return nil, fmt.Errorf("cannot convert NaN to integer") + } else if n.InfinityModifier != Finite { + return nil, fmt.Errorf("cannot convert %v to integer", n.InfinityModifier) + } + + if n.Int == nil { + return big.NewInt(0), nil + } + if n.Exp == 0 { return n.Int, nil } @@ -173,40 +214,41 @@ func (n *Numeric) toBigInt() (*big.Int, error) { } func parseNumericString(str string) (n *big.Int, exp int32, err error) { - idx := strings.IndexByte(str, '.') + // Keep str intact so errors report what the caller actually passed in. + digits := str + idx := strings.IndexByte(digits, '.') if idx == -1 { - for len(str) > 1 && str[len(str)-1] == '0' && str[len(str)-2] != '-' { - str = str[:len(str)-1] + for len(digits) > 1 && digits[len(digits)-1] == '0' && digits[len(digits)-2] != '-' { + digits = digits[:len(digits)-1] exp++ } } else { - exp = int32(-(len(str) - idx - 1)) - str = str[:idx] + str[idx+1:] + exp = int32(-(len(digits) - idx - 1)) + digits = digits[:idx] + digits[idx+1:] } accum := &big.Int{} - if _, ok := accum.SetString(str, 10); !ok { + if _, ok := accum.SetString(digits, 10); !ok { return nil, 0, fmt.Errorf("%s is not a number", str) } return accum, exp, nil } -func nbaseDigitsToInt64(src []byte) (accum int64, bytesRead, digitsRead int) { - digits := min(len(src)/2, 4) - - rp := 0 +// nbaseDigitsToInt64 reads up to 4 nbase digits and packs them into an int64. +// It stops early at digitsLeft or at the end of r, whichever comes first. +func nbaseDigitsToInt64(r *pgio.Reader, digitsLeft int) (accum int64, digitsRead int) { + digits := min(digitsLeft, r.Remaining()/2, 4) for i := range digits { if i > 0 { accum *= nbase } - accum += int64(binary.BigEndian.Uint16(src[rp:])) - rp += 2 + accum += int64(r.Uint16()) } - return accum, rp, digits + return accum, digits } // Scan implements the [database/sql.Scanner] interface. @@ -242,8 +284,13 @@ func (n Numeric) MarshalJSON() ([]byte, error) { return []byte("null"), nil } - if n.NaN { + switch { + case n.NaN: return []byte(`"NaN"`), nil + case n.InfinityModifier == Infinity: + return []byte(`"Infinity"`), nil + case n.InfinityModifier == NegativeInfinity: + return []byte(`"-Infinity"`), nil } return n.numberTextBytes(), nil @@ -259,7 +306,17 @@ func (n *Numeric) UnmarshalJSON(src []byte) error { *n = Numeric{NaN: true, Valid: true} return nil } - return scanPlanTextAnyToNumericScanner{}.Scan(src, n) + if bytes.Equal(src, []byte(`"Infinity"`)) { + *n = Numeric{InfinityModifier: Infinity, Valid: true} + return nil + } + if bytes.Equal(src, []byte(`"-Infinity"`)) { + *n = Numeric{InfinityModifier: NegativeInfinity, Valid: true} + return nil + } + // JSON numbers may use scientific notation even when the producer did not + // write it that way: encoding/json emits 1e+21 for float64(1e21). + return n.ScanScientific(string(src)) } // numberString returns a string of the number. undefined if NaN, infinite, or NULL @@ -416,6 +473,14 @@ func encodeNumericBinary(n Numeric, buf []byte) (newBuf []byte, err error) { sign = 16384 } + // The binary format stores ndigits as uint16 and weight and dscale as int16, + // so values that do not fit must be rejected rather than silently truncated. + // Exp maps directly onto dscale, so check it before doing any big.Int work: a very + // negative exponent would otherwise build an enormous divisor below. + if n.Exp < -math.MaxInt16 { + return nil, fmt.Errorf("cannot encode numeric: exponent %d is out of range", n.Exp) + } + absInt := &big.Int{} wholePart := &big.Int{} fracPart := &big.Int{} @@ -443,7 +508,7 @@ func encodeNumericBinary(n Numeric, buf []byte) (newBuf []byte, err error) { if exp < 0 { divisor := &big.Int{} - divisor.Exp(big10, big.NewInt(int64(-exp)), nil) + divisor.Exp(big10, big.NewInt(-int64(exp)), nil) wholePart.DivMod(absInt, divisor, fracPart) fracPart.Add(fracPart, divisor) } else { @@ -464,18 +529,25 @@ func encodeNumericBinary(n Numeric, buf []byte) (newBuf []byte, err error) { } } - buf = pgio.AppendInt16(buf, int16(len(wholeDigits)+len(fracDigits))) + ndigits := len(wholeDigits) + len(fracDigits) + if ndigits > math.MaxUint16 { + return nil, fmt.Errorf("cannot encode numeric: %d digits is out of range", ndigits) + } + buf = pgio.AppendUint16(buf, uint16(ndigits)) - var weight int16 + var weight int64 if len(wholeDigits) > 0 { - weight = int16(len(wholeDigits) - 1) + weight = int64(len(wholeDigits) - 1) if exp > 0 { - weight += int16(exp / 4) + weight += int64(exp) / 4 } } else { - weight = int16(exp/4) - 1 + int16(len(fracDigits)) + weight = int64(exp)/4 - 1 + int64(len(fracDigits)) + } + if weight > math.MaxInt16 || weight < math.MinInt16 { + return nil, fmt.Errorf("cannot encode numeric: exponent %d is out of range", n.Exp) } - buf = pgio.AppendInt16(buf, weight) + buf = pgio.AppendInt16(buf, int16(weight)) buf = pgio.AppendInt16(buf, sign) @@ -600,25 +672,21 @@ func (NumericCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanP type scanPlanBinaryNumericToNumericScanner struct{} func (scanPlanBinaryNumericToNumericScanner) Scan(src []byte, dst any) error { - scanner := (dst).(NumericScanner) + scanner := dst.(NumericScanner) if src == nil { return scanner.ScanNumeric(Numeric{}) } - if len(src) < 8 { - return fmt.Errorf("numeric incomplete %v", src) - } + r := pgio.NewReader(src) - rp := 0 - ndigits := binary.BigEndian.Uint16(src[rp:]) - rp += 2 - weight := int16(binary.BigEndian.Uint16(src[rp:])) - rp += 2 - sign := binary.BigEndian.Uint16(src[rp:]) - rp += 2 - dscale := int16(binary.BigEndian.Uint16(src[rp:])) - rp += 2 + ndigits := r.Uint16() + weight := r.Int16() + sign := r.Uint16() + dscale := r.Int16() + if err := r.Err(); err != nil { + return fmt.Errorf("numeric incomplete: %w", err) + } switch sign { case pgNumericNaNSign: @@ -633,15 +701,16 @@ func (scanPlanBinaryNumericToNumericScanner) Scan(src []byte, dst any) error { return scanner.ScanNumeric(Numeric{Int: big.NewInt(0), Valid: true}) } - if len(src[rp:]) < int(ndigits)*2 { + if r.Remaining() < int(ndigits)*2 { return fmt.Errorf("numeric incomplete %v", src) } accum := &big.Int{} - for i := 0; i < int(ndigits+3)/4; i++ { - int64accum, bytesRead, digitsRead := nbaseDigitsToInt64(src[rp:]) - rp += bytesRead + // int(ndigits) before the addition: ndigits is a uint16, so ndigits+3 + // would wrap for counts above 65532 and skip the loop entirely. + for i := 0; i < (int(ndigits)+3)/4; i++ { + int64accum, digitsRead := nbaseDigitsToInt64(r, int(ndigits)-i*4) if i > 0 { var mul *big.Int @@ -663,6 +732,10 @@ func (scanPlanBinaryNumericToNumericScanner) Scan(src []byte, dst any) error { accum.Add(accum, big.NewInt(int64accum)) } + if err := r.Finish(); err != nil { + return fmt.Errorf("numeric: %w", err) + } + exp := (int32(weight) - int32(ndigits) + 1) * 4 if dscale > 0 { @@ -687,7 +760,7 @@ func (scanPlanBinaryNumericToNumericScanner) Scan(src []byte, dst any) error { reduced := &big.Int{} remainder := &big.Int{} - if exp >= 0 { + if exp >= 0 && accum.Sign() != 0 { for { reduced.DivMod(accum, big10, remainder) if remainder.Sign() != 0 { @@ -708,7 +781,7 @@ func (scanPlanBinaryNumericToNumericScanner) Scan(src []byte, dst any) error { type scanPlanBinaryNumericToFloat64Scanner struct{} func (scanPlanBinaryNumericToFloat64Scanner) Scan(src []byte, dst any) error { - scanner := (dst).(Float64Scanner) + scanner := dst.(Float64Scanner) if src == nil { return scanner.ScanFloat64(Float8{}) @@ -732,7 +805,7 @@ func (scanPlanBinaryNumericToFloat64Scanner) Scan(src []byte, dst any) error { type scanPlanBinaryNumericToInt64Scanner struct{} func (scanPlanBinaryNumericToInt64Scanner) Scan(src []byte, dst any) error { - scanner := (dst).(Int64Scanner) + scanner := dst.(Int64Scanner) if src == nil { return scanner.ScanInt64(Int8{}) @@ -760,7 +833,7 @@ func (scanPlanBinaryNumericToInt64Scanner) Scan(src []byte, dst any) error { type scanPlanBinaryNumericToTextScanner struct{} func (scanPlanBinaryNumericToTextScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TextScanner) + scanner := dst.(TextScanner) if src == nil { return scanner.ScanText(Text{}) @@ -784,7 +857,7 @@ func (scanPlanBinaryNumericToTextScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToNumericScanner struct{} func (scanPlanTextAnyToNumericScanner) Scan(src []byte, dst any) error { - scanner := (dst).(NumericScanner) + scanner := dst.(NumericScanner) if src == nil { return scanner.ScanNumeric(Numeric{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/path.go b/vendor/github.com/jackc/pgx/v5/pgtype/path.go index 6398b58..0afceac 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/path.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/path.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "math" "strconv" @@ -172,34 +171,32 @@ func (PathCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan type scanPlanBinaryPathToPathScanner struct{} func (scanPlanBinaryPathToPathScanner) Scan(src []byte, dst any) error { - scanner := (dst).(PathScanner) + scanner := dst.(PathScanner) if src == nil { return scanner.ScanPath(Path{}) } - if len(src) < 5 { - return fmt.Errorf("invalid length for Path: %v", len(src)) - } - - closed := src[0] == 1 - pointCount := int(binary.BigEndian.Uint32(src[1:])) + r := pgio.NewReader(src) - rp := 5 - - if 5+pointCount*16 != len(src) { - return fmt.Errorf("invalid length for Path with %d points: %v", pointCount, len(src)) + closed := r.Byte() == 1 + // Each point is two float64s. + pointCount := r.Count(16) + if err := r.Err(); err != nil { + return fmt.Errorf("invalid length for Path: %w", err) } points := make([]Vec2, pointCount) for i := range points { - x := binary.BigEndian.Uint64(src[rp:]) - rp += 8 - y := binary.BigEndian.Uint64(src[rp:]) - rp += 8 + x := r.Uint64() + y := r.Uint64() points[i] = Vec2{math.Float64frombits(x), math.Float64frombits(y)} } + if err := r.Finish(); err != nil { + return fmt.Errorf("invalid length for Path with %d points: %w", pointCount, err) + } + return scanner.ScanPath(Path{ P: points, Closed: closed, @@ -210,7 +207,7 @@ func (scanPlanBinaryPathToPathScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToPathScanner struct{} func (scanPlanTextAnyToPathScanner) Scan(src []byte, dst any) error { - scanner := (dst).(PathScanner) + scanner := dst.(PathScanner) if src == nil { return scanner.ScanPath(Path{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/pgtype.go b/vendor/github.com/jackc/pgx/v5/pgtype/pgtype.go index 46b892b..98f5b3b 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/pgtype.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/pgtype.go @@ -412,7 +412,7 @@ func (scanPlanString) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - p := (dst).(*string) + p := dst.(*string) *p = string(src) return nil } @@ -497,7 +497,12 @@ type pointerPointerScanPlan struct { func (plan *pointerPointerScanPlan) SetNext(next ScanPlan) { plan.next = next } func (plan *pointerPointerScanPlan) Scan(src []byte, dst any) error { - el := reflect.ValueOf(dst).Elem() + dstValue := reflect.ValueOf(dst) + if dstValue.Kind() != reflect.Pointer || dstValue.IsNil() { + return fmt.Errorf("cannot scan into non-pointer or nil destinations %T", dst) + } + + el := dstValue.Elem() if src == nil { el.Set(reflect.Zero(el.Type())) return nil @@ -510,11 +515,11 @@ func (plan *pointerPointerScanPlan) Scan(src []byte, dst any) error { // TryPointerPointerScanPlan handles a pointer to a pointer by setting the target to nil for SQL NULL and allocating and // scanning for non-NULL. func TryPointerPointerScanPlan(target any) (plan WrappedScanPlanNextSetter, nextTarget any, ok bool) { - if dstValue := reflect.ValueOf(target); dstValue.Kind() == reflect.Pointer { - elemValue := dstValue.Elem() - if elemValue.Kind() == reflect.Pointer { - plan = &pointerPointerScanPlan{dstType: dstValue.Type()} - return plan, reflect.Zero(elemValue.Type()).Interface(), true + if dstType := reflect.TypeOf(target); dstType != nil && dstType.Kind() == reflect.Pointer { + elemType := dstType.Elem() + if elemType.Kind() == reflect.Pointer { + plan = &pointerPointerScanPlan{dstType: dstType} + return plan, reflect.Zero(elemType).Interface(), true } } @@ -575,8 +580,7 @@ func TryFindUnderlyingTypeScanPlan(dst any) (plan WrappedScanPlanNextSetter, nex if nextDstType == nil { if elemValue.Kind() == reflect.Slice { if elemValue.Type().Elem().Kind() == reflect.Uint8 { - var v *[]byte - nextDstType = reflect.TypeOf(v) + nextDstType = reflect.TypeFor[*[]byte]() } } @@ -1828,19 +1832,19 @@ func TryWrapSliceEncodePlan(value any) (plan WrappedEncodePlanNextSetter, nextVa // Avoid using reflect path for common types. switch value := value.(type) { case []int16: - return &wrapSliceEncodePlan[int16]{}, (FlatArray[int16])(value), true + return &wrapSliceEncodePlan[int16]{}, FlatArray[int16](value), true case []int32: - return &wrapSliceEncodePlan[int32]{}, (FlatArray[int32])(value), true + return &wrapSliceEncodePlan[int32]{}, FlatArray[int32](value), true case []int64: - return &wrapSliceEncodePlan[int64]{}, (FlatArray[int64])(value), true + return &wrapSliceEncodePlan[int64]{}, FlatArray[int64](value), true case []float32: - return &wrapSliceEncodePlan[float32]{}, (FlatArray[float32])(value), true + return &wrapSliceEncodePlan[float32]{}, FlatArray[float32](value), true case []float64: - return &wrapSliceEncodePlan[float64]{}, (FlatArray[float64])(value), true + return &wrapSliceEncodePlan[float64]{}, FlatArray[float64](value), true case []string: - return &wrapSliceEncodePlan[string]{}, (FlatArray[string])(value), true + return &wrapSliceEncodePlan[string]{}, FlatArray[string](value), true case []time.Time: - return &wrapSliceEncodePlan[time.Time]{}, (FlatArray[time.Time])(value), true + return &wrapSliceEncodePlan[time.Time]{}, FlatArray[time.Time](value), true } if valueType := reflect.TypeOf(value); valueType != nil && valueType.Kind() == reflect.Slice { @@ -1860,7 +1864,7 @@ type wrapSliceEncodePlan[T any] struct { func (plan *wrapSliceEncodePlan[T]) SetNext(next EncodePlan) { plan.next = next } func (plan *wrapSliceEncodePlan[T]) Encode(value any, buf []byte) (newBuf []byte, err error) { - return plan.next.Encode((FlatArray[T])(value.([]T)), buf) + return plan.next.Encode(FlatArray[T](value.([]T)), buf) } type wrapSliceEncodeReflectPlan struct { @@ -1999,6 +2003,9 @@ func (m *Map) Encode(oid uint32, formatCode int16, value any, buf []byte) (newBu // // This uses the type of v to look up the PostgreSQL OID that v presumably came from. This means v must be registered // with m by calling RegisterDefaultPgType. +// +// As of Go 1.27, database/sql calls the driver directly to scan columns when using pgx's stdlib package, so this is +// no longer necessary. func (m *Map) SQLScanner(v any) sql.Scanner { if s, ok := v.(sql.Scanner); ok { return s diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/pgtype_default.go b/vendor/github.com/jackc/pgx/v5/pgtype/pgtype_default.go index 42b39d8..2b9e8fe 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/pgtype_default.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/pgtype_default.go @@ -131,7 +131,7 @@ func initDefaultMap() { defaultMap.RegisterType(&Type{Name: "_aclitem", OID: ACLItemArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[ACLItemOID]}}) defaultMap.RegisterType(&Type{Name: "_bit", OID: BitArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[BitOID]}}) defaultMap.RegisterType(&Type{Name: "_bool", OID: BoolArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[BoolOID]}}) - defaultMap.RegisterType(&Type{Name: "_box", OID: BoxArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[BoxOID]}}) + defaultMap.RegisterType(&Type{Name: "_box", OID: BoxArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[BoxOID], Delimiter: ';'}}) defaultMap.RegisterType(&Type{Name: "_bpchar", OID: BPCharArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[BPCharOID]}}) defaultMap.RegisterType(&Type{Name: "_bytea", OID: ByteaArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[ByteaOID]}}) defaultMap.RegisterType(&Type{Name: "_char", OID: QCharArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[QCharOID]}}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/point.go b/vendor/github.com/jackc/pgx/v5/pgtype/point.go index d90cb70..4536e1e 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/point.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/point.go @@ -3,7 +3,6 @@ package pgtype import ( "bytes" "database/sql/driver" - "encoding/binary" "fmt" "math" "strconv" @@ -215,18 +214,20 @@ func (c PointCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (a type scanPlanBinaryPointToPointScanner struct{} func (scanPlanBinaryPointToPointScanner) Scan(src []byte, dst any) error { - scanner := (dst).(PointScanner) + scanner := dst.(PointScanner) if src == nil { return scanner.ScanPoint(Point{}) } - if len(src) != 16 { - return fmt.Errorf("invalid length for point: %v", len(src)) - } + r := pgio.NewReader(src) - x := binary.BigEndian.Uint64(src) - y := binary.BigEndian.Uint64(src[8:]) + x := r.Uint64() + y := r.Uint64() + + if err := r.Finish(); err != nil { + return fmt.Errorf("point: %w", err) + } return scanner.ScanPoint(Point{ P: Vec2{math.Float64frombits(x), math.Float64frombits(y)}, @@ -237,7 +238,7 @@ func (scanPlanBinaryPointToPointScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToPointScanner struct{} func (scanPlanTextAnyToPointScanner) Scan(src []byte, dst any) error { - scanner := (dst).(PointScanner) + scanner := dst.(PointScanner) if src == nil { return scanner.ScanPoint(Point{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/polygon.go b/vendor/github.com/jackc/pgx/v5/pgtype/polygon.go index 34aa0a6..c9e2df0 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/polygon.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/polygon.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "math" "strconv" @@ -157,32 +156,31 @@ func (PolygonCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanP type scanPlanBinaryPolygonToPolygonScanner struct{} func (scanPlanBinaryPolygonToPolygonScanner) Scan(src []byte, dst any) error { - scanner := (dst).(PolygonScanner) + scanner := dst.(PolygonScanner) if src == nil { return scanner.ScanPolygon(Polygon{}) } - if len(src) < 5 { - return fmt.Errorf("invalid length for polygon: %v", len(src)) - } - - pointCount := int(binary.BigEndian.Uint32(src)) - rp := 4 + r := pgio.NewReader(src) - if 4+pointCount*16 != len(src) { - return fmt.Errorf("invalid length for Polygon with %d points: %v", pointCount, len(src)) + // Each point is two float64s. + pointCount := r.Count(16) + if err := r.Err(); err != nil { + return fmt.Errorf("invalid length for polygon: %w", err) } points := make([]Vec2, pointCount) for i := range points { - x := binary.BigEndian.Uint64(src[rp:]) - rp += 8 - y := binary.BigEndian.Uint64(src[rp:]) - rp += 8 + x := r.Uint64() + y := r.Uint64() points[i] = Vec2{math.Float64frombits(x), math.Float64frombits(y)} } + if err := r.Finish(); err != nil { + return fmt.Errorf("invalid length for Polygon with %d points: %w", pointCount, err) + } + return scanner.ScanPolygon(Polygon{ P: points, Valid: true, @@ -192,7 +190,7 @@ func (scanPlanBinaryPolygonToPolygonScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToPolygonScanner struct{} func (scanPlanTextAnyToPolygonScanner) Scan(src []byte, dst any) error { - scanner := (dst).(PolygonScanner) + scanner := dst.(PolygonScanner) if src == nil { return scanner.ScanPolygon(Polygon{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/range.go b/vendor/github.com/jackc/pgx/v5/pgtype/range.go index dec153e..dba88fb 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/range.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/range.go @@ -2,8 +2,9 @@ package pgtype import ( "bytes" - "encoding/binary" "fmt" + + "github.com/jackc/pgx/v5/internal/pgio" ) type BoundType byte @@ -201,17 +202,16 @@ const ( func parseUntypedBinaryRange(src []byte) (*untypedBinaryRange, error) { ubr := &untypedBinaryRange{} + r := pgio.NewReader(src) - if len(src) == 0 { + rangeType := r.Byte() + if r.Err() != nil { return nil, fmt.Errorf("range too short: %v", len(src)) } - rangeType := src[0] - rp := 1 - if rangeType&emptyMask > 0 { - if len(src[rp:]) > 0 { - return nil, fmt.Errorf("unexpected trailing bytes parsing empty range: %v", len(src[rp:])) + if err := r.Finish(); err != nil { + return nil, fmt.Errorf("empty range: %w", err) } ubr.LowerType = Empty ubr.UpperType = Empty @@ -236,50 +236,24 @@ func parseUntypedBinaryRange(src []byte) (*untypedBinaryRange, error) { ubr.UpperType = Exclusive } - if ubr.LowerType == Unbounded && ubr.UpperType == Unbounded { - if len(src[rp:]) > 0 { - return nil, fmt.Errorf("unexpected trailing bytes parsing unbounded range: %v", len(src[rp:])) - } - return ubr, nil - } - - if len(src[rp:]) < 4 { - return nil, fmt.Errorf("too few bytes for size: %v", src[rp:]) - } - valueLen := int(binary.BigEndian.Uint32(src[rp:])) - rp += 4 - - if valueLen < 0 || len(src[rp:]) < valueLen { - return nil, fmt.Errorf("range lower bound length %d exceeds remaining %d bytes", valueLen, len(src[rp:])) - } - val := src[rp : rp+valueLen] - rp += valueLen - if ubr.LowerType != Unbounded { - ubr.Lower = val - } else { - ubr.Upper = val - if len(src[rp:]) > 0 { - return nil, fmt.Errorf("unexpected trailing bytes parsing range: %v", len(src[rp:])) + val, null := r.Value() + if null { + return nil, fmt.Errorf("range lower bound cannot be NULL") } - return ubr, nil + ubr.Lower = val } if ubr.UpperType != Unbounded { - if len(src[rp:]) < 4 { - return nil, fmt.Errorf("too few bytes for size: %v", src[rp:]) + val, null := r.Value() + if null { + return nil, fmt.Errorf("range upper bound cannot be NULL") } - valueLen := int(binary.BigEndian.Uint32(src[rp:])) - rp += 4 - if valueLen < 0 || len(src[rp:]) < valueLen { - return nil, fmt.Errorf("range upper bound length %d exceeds remaining %d bytes", valueLen, len(src[rp:])) - } - ubr.Upper = src[rp : rp+valueLen] - rp += valueLen + ubr.Upper = val } - if len(src[rp:]) > 0 { - return nil, fmt.Errorf("unexpected trailing bytes parsing range: %v", len(src[rp:])) + if err := r.Finish(); err != nil { + return nil, fmt.Errorf("range: %w", err) } return ubr, nil diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/range_codec.go b/vendor/github.com/jackc/pgx/v5/pgtype/range_codec.go index dc1ac8b..81ad206 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/range_codec.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/range_codec.go @@ -3,6 +3,7 @@ package pgtype import ( "database/sql/driver" "fmt" + "strings" "github.com/jackc/pgx/v5/internal/pgio" ) @@ -192,6 +193,7 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) return nil, fmt.Errorf("cannot encode %v as element of range", lower) } + boundStart := len(buf) buf, err = lowerPlan.Encode(lower, buf) if err != nil { return nil, fmt.Errorf("failed to encode %v as element of range: %w", lower, err) @@ -199,6 +201,7 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) if buf == nil { return nil, fmt.Errorf("Lower cannot be NULL unless LowerType is Unbounded") } + buf = append(buf[:boundStart], quoteRangeBoundIfNeeded(string(buf[boundStart:]))...) } buf = append(buf, ',') @@ -213,6 +216,7 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) return nil, fmt.Errorf("cannot encode %v as element of range", upper) } + boundStart := len(buf) buf, err = upperPlan.Encode(upper, buf) if err != nil { return nil, fmt.Errorf("failed to encode %v as element of range: %w", upper, err) @@ -220,6 +224,7 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) if buf == nil { return nil, fmt.Errorf("Upper cannot be NULL unless UpperType is Unbounded") } + buf = append(buf[:boundStart], quoteRangeBoundIfNeeded(string(buf[boundStart:]))...) } switch upperType { @@ -234,6 +239,13 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) return buf, nil } +func quoteRangeBoundIfNeeded(src string) string { + if src == "" || strings.ContainsAny(src, "\"\\,()[]") { + return quoteArrayElement(src) + } + return src +} + func (c *RangeCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan { switch format { case BinaryFormatCode: @@ -255,7 +267,7 @@ type scanPlanBinaryRangeToRangeScanner struct { } func (plan *scanPlanBinaryRangeToRangeScanner) Scan(src []byte, target any) error { - rangeScanner := (target).(RangeScanner) + rangeScanner := target.(RangeScanner) if src == nil { return rangeScanner.ScanNull() @@ -305,7 +317,7 @@ type scanPlanTextRangeToRangeScanner struct { } func (plan *scanPlanTextRangeToRangeScanner) Scan(src []byte, target any) error { - rangeScanner := (target).(RangeScanner) + rangeScanner := target.(RangeScanner) if src == nil { return rangeScanner.ScanNull() diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/record_codec.go b/vendor/github.com/jackc/pgx/v5/pgtype/record_codec.go index a663e4d..6fef94e 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/record_codec.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/record_codec.go @@ -40,7 +40,7 @@ type scanPlanBinaryRecordToCompositeIndexScanner struct { } func (plan *scanPlanBinaryRecordToCompositeIndexScanner) Scan(src []byte, target any) error { - targetScanner := (target).(CompositeIndexScanner) + targetScanner := target.(CompositeIndexScanner) if src == nil { return targetScanner.ScanNull() @@ -48,14 +48,17 @@ func (plan *scanPlanBinaryRecordToCompositeIndexScanner) Scan(src []byte, target scanner := NewCompositeBinaryScanner(plan.m, src) for i := 0; scanner.Next(); i++ { - fieldTarget := targetScanner.ScanIndex(i) + fieldTarget, err := compositeFieldTarget(targetScanner, i) + if err != nil { + return err + } if fieldTarget != nil { fieldPlan := plan.m.PlanScan(scanner.OID(), BinaryFormatCode, fieldTarget) if fieldPlan == nil { return fmt.Errorf("unable to scan OID %d in binary format into %v", scanner.OID(), fieldTarget) } - err := fieldPlan.Scan(scanner.Bytes(), fieldTarget) + err = fieldPlan.Scan(scanner.Bytes(), fieldTarget) if err != nil { return err } @@ -96,8 +99,11 @@ func (RecordCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (an return string(src), nil case BinaryFormatCode: scanner := NewCompositeBinaryScanner(m, src) - values := make([]any, scanner.FieldCount()) - for i := 0; scanner.Next(); i++ { + // The field count is a hint for the initial allocation only. Append the + // values actually present rather than indexing into a presized slice, as + // the source may carry more or fewer fields than the header claims. + values := make([]any, 0, scanner.FieldCount()) + for scanner.Next() { var v any fieldPlan := m.PlanScan(scanner.OID(), BinaryFormatCode, &v) if fieldPlan == nil { @@ -109,7 +115,7 @@ func (RecordCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (an return nil, err } - values[i] = v + values = append(values, v) } if err := scanner.Err(); err != nil { diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/text.go b/vendor/github.com/jackc/pgx/v5/pgtype/text.go index e08b125..f20cf2a 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/text.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/text.go @@ -186,7 +186,7 @@ func (scanPlanTextAnyToString) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - p := (dst).(*string) + p := dst.(*string) *p = string(src) return nil @@ -195,7 +195,7 @@ func (scanPlanTextAnyToString) Scan(src []byte, dst any) error { type scanPlanAnyToNewByteSlice struct{} func (scanPlanAnyToNewByteSlice) Scan(src []byte, dst any) error { - p := (dst).(*[]byte) + p := dst.(*[]byte) if src == nil { *p = nil } else { @@ -209,14 +209,14 @@ func (scanPlanAnyToNewByteSlice) Scan(src []byte, dst any) error { type scanPlanAnyToByteScanner struct{} func (scanPlanAnyToByteScanner) Scan(src []byte, dst any) error { - p := (dst).(BytesScanner) + p := dst.(BytesScanner) return p.ScanBytes(src) } type scanPlanTextAnyToTextScanner struct{} func (scanPlanTextAnyToTextScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TextScanner) + scanner := dst.(TextScanner) if src == nil { return scanner.ScanText(Text{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/tid.go b/vendor/github.com/jackc/pgx/v5/pgtype/tid.go index 98d067a..a572e86 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/tid.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/tid.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "strconv" "strings" @@ -152,19 +151,24 @@ func (TIDCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan type scanPlanBinaryTIDToTIDScanner struct{} func (scanPlanBinaryTIDToTIDScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TIDScanner) + scanner := dst.(TIDScanner) if src == nil { return scanner.ScanTID(TID{}) } - if len(src) != 6 { - return fmt.Errorf("invalid length for tid: %v", len(src)) + r := pgio.NewReader(src) + + blockNumber := r.Uint32() + offsetNumber := r.Uint16() + + if err := r.Finish(); err != nil { + return fmt.Errorf("tid: %w", err) } return scanner.ScanTID(TID{ - BlockNumber: binary.BigEndian.Uint32(src), - OffsetNumber: binary.BigEndian.Uint16(src[4:]), + BlockNumber: blockNumber, + OffsetNumber: offsetNumber, Valid: true, }) } @@ -172,18 +176,20 @@ func (scanPlanBinaryTIDToTIDScanner) Scan(src []byte, dst any) error { type scanPlanBinaryTIDToTextScanner struct{} func (scanPlanBinaryTIDToTextScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TextScanner) + scanner := dst.(TextScanner) if src == nil { return scanner.ScanText(Text{}) } - if len(src) != 6 { - return fmt.Errorf("invalid length for tid: %v", len(src)) - } + r := pgio.NewReader(src) + + blockNumber := r.Uint32() + offsetNumber := r.Uint16() - blockNumber := binary.BigEndian.Uint32(src) - offsetNumber := binary.BigEndian.Uint16(src[4:]) + if err := r.Finish(); err != nil { + return fmt.Errorf("tid: %w", err) + } return scanner.ScanText(Text{ String: fmt.Sprintf(`(%d,%d)`, blockNumber, offsetNumber), @@ -194,7 +200,7 @@ func (scanPlanBinaryTIDToTextScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToTIDScanner struct{} func (scanPlanTextAnyToTIDScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TIDScanner) + scanner := dst.(TIDScanner) if src == nil { return scanner.ScanTID(TID{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/time.go b/vendor/github.com/jackc/pgx/v5/pgtype/time.go index 72cdb50..a8c2fdb 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/time.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/time.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "strconv" @@ -158,17 +157,18 @@ func (TimeCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan type scanPlanBinaryTimeToTimeScanner struct{} func (scanPlanBinaryTimeToTimeScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TimeScanner) + scanner := dst.(TimeScanner) if src == nil { return scanner.ScanTime(Time{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for time: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("time: %w", err) } - usec := int64(binary.BigEndian.Uint64(src)) + usec := int64(raw) return scanner.ScanTime(Time{Microseconds: usec, Valid: true}) } @@ -176,7 +176,7 @@ func (scanPlanBinaryTimeToTimeScanner) Scan(src []byte, dst any) error { type scanPlanBinaryTimeToTextScanner struct{} func (scanPlanBinaryTimeToTextScanner) Scan(src []byte, dst any) error { - ts, ok := (dst).(TextScanner) + ts, ok := dst.(TextScanner) if !ok { return ErrScanTargetTypeChanged } @@ -185,11 +185,12 @@ func (scanPlanBinaryTimeToTextScanner) Scan(src []byte, dst any) error { return ts.ScanText(Text{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for time: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("time: %w", err) } - usec := int64(binary.BigEndian.Uint64(src)) + usec := int64(raw) tim := Time{Microseconds: usec, Valid: true} @@ -204,7 +205,7 @@ func (scanPlanBinaryTimeToTextScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToTimeScanner struct{} func (scanPlanTextAnyToTimeScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TimeScanner) + scanner := dst.(TimeScanner) if src == nil { return scanner.ScanTime(Time{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/timestamp.go b/vendor/github.com/jackc/pgx/v5/pgtype/timestamp.go index 405c77e..33c2b94 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/timestamp.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/timestamp.go @@ -2,18 +2,16 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "encoding/json" "fmt" - "strings" "time" + "github.com/jackc/pgx/v5/internal/pgdatetime" "github.com/jackc/pgx/v5/internal/pgio" ) const ( - pgTimestampFormat = "2006-01-02 15:04:05.999999999" - jsonISO8601 = "2006-01-02T15:04:05.999999999" + jsonISO8601 = "2006-01-02T15:04:05.999999999" ) type TimestampScanner interface { @@ -201,33 +199,17 @@ func (encodePlanTimestampCodecText) Encode(value any, buf []byte) (newBuf []byte return nil, nil } - var s string - switch ts.InfinityModifier { case Finite: - t := discardTimeZone(ts.Time) - - // Year 0000 is 1 BC - bc := false - if year := t.Year(); year <= 0 { - year = -year + 1 - t = time.Date(year, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC) - bc = true - } - - s = t.Truncate(time.Microsecond).Format(pgTimestampFormat) - - if bc { - s += " BC" - } + // The fields are read in ts.Time's own location, so there is no zone to discard + // and nothing to append after the time. + buf = pgdatetime.AppendTimestamp(buf, ts.Time, "") case Infinity: - s = "infinity" + buf = append(buf, "infinity"...) case NegativeInfinity: - s = "-infinity" + buf = append(buf, "-infinity"...) } - buf = append(buf, s...) - return buf, nil } @@ -257,18 +239,19 @@ func (c *TimestampCodec) PlanScan(m *Map, oid uint32, format int16, target any) type scanPlanBinaryTimestampToTimestampScanner struct{ location *time.Location } func (plan *scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TimestampScanner) + scanner := dst.(TimestampScanner) if src == nil { return scanner.ScanTimestamp(Timestamp{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for timestamp: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("timestamp: %w", err) } var ts Timestamp - microsecSinceY2K := int64(binary.BigEndian.Uint64(src)) + microsecSinceY2K := int64(raw) switch microsecSinceY2K { case infinityMicrosecondOffset: @@ -280,6 +263,9 @@ func (plan *scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) microsecFromUnixEpochToY2K/1_000_000+microsecSinceY2K/1_000_000, (microsecFromUnixEpochToY2K%1_000_000*1_000)+(microsecSinceY2K%1_000_000*1000), ).UTC() + if tim.Before(minDateTime) || !tim.Before(endTimestamp) { + return fmt.Errorf("timestamp %d microseconds from 2000-01-01 is out of range", microsecSinceY2K) + } if plan.location != nil { tim = time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), plan.location) } @@ -292,37 +278,34 @@ func (plan *scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) type scanPlanTextTimestampToTimestampScanner struct{ location *time.Location } func (plan *scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TimestampScanner) + scanner := dst.(TimestampScanner) if src == nil { return scanner.ScanTimestamp(Timestamp{}) } + dt, err := parseTextDateTime(src) + if err != nil { + return err + } + var ts Timestamp - sbuf := string(src) - switch sbuf { - case "infinity": - ts = Timestamp{Valid: true, InfinityModifier: Infinity} - case "-infinity": - ts = Timestamp{Valid: true, InfinityModifier: -Infinity} - default: - bc := false - if strings.HasSuffix(sbuf, " BC") { - sbuf = sbuf[:len(sbuf)-3] - bc = true + if dt.infinity != Finite { + ts = Timestamp{Valid: true, InfinityModifier: dt.infinity} + } else { + if !dt.hasTime || dt.hasOffset { + return badDateTime(src) } - tim, err := time.Parse(pgTimestampFormat, sbuf) + + tim, err := dt.toTime(src, "timestamp", endTimestamp) if err != nil { return err } - if bc { - year := -tim.Year() + 1 - tim = time.Date(year, tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), tim.Location()) - } - + // timestamp has no time zone, so ScanLocation reinterprets the same wall clock + // reading rather than converting the instant. if plan.location != nil { - tim = time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), plan.location) + tim = dt.in(plan.location) } ts = Timestamp{Time: tim, Valid: true} diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/timestamptz.go b/vendor/github.com/jackc/pgx/v5/pgtype/timestamptz.go index 139312a..f05e384 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/timestamptz.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/timestamptz.go @@ -2,19 +2,15 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "encoding/json" "fmt" - "strings" "time" + "github.com/jackc/pgx/v5/internal/pgdatetime" "github.com/jackc/pgx/v5/internal/pgio" ) const ( - pgTimestamptzHourFormat = "2006-01-02 15:04:05.999999999Z07" - pgTimestamptzMinuteFormat = "2006-01-02 15:04:05.999999999Z07:00" - pgTimestamptzSecondFormat = "2006-01-02 15:04:05.999999999Z07:00:00" microsecFromUnixEpochToY2K = 946_684_800 * 1_000_000 ) @@ -199,34 +195,15 @@ func (encodePlanTimestamptzCodecText) Encode(value any, buf []byte) (newBuf []by return nil, nil } - var s string - switch ts.InfinityModifier { case Finite: - - t := ts.Time.UTC().Truncate(time.Microsecond) - - // Year 0000 is 1 BC - bc := false - if year := t.Year(); year <= 0 { - year = -year + 1 - t = time.Date(year, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC) - bc = true - } - - s = t.Format(pgTimestamptzSecondFormat) - - if bc { - s += " BC" - } + buf = pgdatetime.AppendTimestamp(buf, ts.Time.UTC(), "Z") case Infinity: - s = "infinity" + buf = append(buf, "infinity"...) case NegativeInfinity: - s = "-infinity" + buf = append(buf, "-infinity"...) } - buf = append(buf, s...) - return buf, nil } @@ -248,18 +225,19 @@ func (c *TimestamptzCodec) PlanScan(m *Map, oid uint32, format int16, target any type scanPlanBinaryTimestamptzToTimestamptzScanner struct{ location *time.Location } func (plan *scanPlanBinaryTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TimestamptzScanner) + scanner := dst.(TimestamptzScanner) if src == nil { return scanner.ScanTimestamptz(Timestamptz{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for timestamptz: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("timestamptz: %w", err) } var tstz Timestamptz - microsecSinceY2K := int64(binary.BigEndian.Uint64(src)) + microsecSinceY2K := int64(raw) switch microsecSinceY2K { case infinityMicrosecondOffset: @@ -271,6 +249,9 @@ func (plan *scanPlanBinaryTimestamptzToTimestamptzScanner) Scan(src []byte, dst microsecFromUnixEpochToY2K/1_000_000+microsecSinceY2K/1_000_000, (microsecFromUnixEpochToY2K%1_000_000*1_000)+(microsecSinceY2K%1_000_000*1_000), ) + if tim.Before(minDateTime) || !tim.Before(endTimestamp) { + return fmt.Errorf("timestamptz %d microseconds from 2000-01-01 is out of range", microsecSinceY2K) + } if plan.location != nil { tim = tim.In(plan.location) } @@ -283,51 +264,38 @@ func (plan *scanPlanBinaryTimestamptzToTimestamptzScanner) Scan(src []byte, dst type scanPlanTextTimestamptzToTimestamptzScanner struct{ location *time.Location } func (plan *scanPlanTextTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TimestamptzScanner) + scanner := dst.(TimestamptzScanner) if src == nil { return scanner.ScanTimestamptz(Timestamptz{}) } - var tstz Timestamptz - sbuf := string(src) - switch sbuf { - case "infinity": - tstz = Timestamptz{Valid: true, InfinityModifier: Infinity} - case "-infinity": - tstz = Timestamptz{Valid: true, InfinityModifier: -Infinity} - default: - bc := false - if strings.HasSuffix(sbuf, " BC") { - sbuf = sbuf[:len(sbuf)-3] - bc = true - } + dt, err := parseTextDateTime(src) + if err != nil { + return err + } - var format string - switch { - case len(sbuf) >= 9 && (sbuf[len(sbuf)-9] == '-' || sbuf[len(sbuf)-9] == '+'): - format = pgTimestamptzSecondFormat - case len(sbuf) >= 6 && (sbuf[len(sbuf)-6] == '-' || sbuf[len(sbuf)-6] == '+'): - format = pgTimestamptzMinuteFormat - default: - format = pgTimestamptzHourFormat + var tstz Timestamptz + if dt.infinity != Finite { + tstz = Timestamptz{Valid: true, InfinityModifier: dt.infinity} + } else { + if !dt.hasTime || !dt.hasOffset { + return badDateTime(src) } - tim, err := time.Parse(format, sbuf) + tim, err := dt.toTime(src, "timestamptz", endTimestamp) if err != nil { return err } - if bc { - year := -tim.Year() + 1 - tim = time.Date(year, tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), tim.Location()) - } - + // The binary path builds its value with time.Unix, which returns time.Local. Match + // it, so that the same value scanned in either format produces the same time.Time. + loc := time.Local if plan.location != nil { - tim = tim.In(plan.location) + loc = plan.location } - tstz = Timestamptz{Time: tim, Valid: true} + tstz = Timestamptz{Time: tim.In(loc), Valid: true} } return scanner.ScanTimestamptz(tstz) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/tsvector.go b/vendor/github.com/jackc/pgx/v5/pgtype/tsvector.go index cc7b831..195481b 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/tsvector.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/tsvector.go @@ -1,9 +1,7 @@ package pgtype import ( - "bytes" "database/sql/driver" - "encoding/binary" "fmt" "strconv" "strings" @@ -184,33 +182,20 @@ func (encodePlanTSVectorCodecBinary) Encode(value any, buf []byte) ([]byte, erro type scanPlanBinaryTSVectorToTSVectorScanner struct{} func (scanPlanBinaryTSVectorToTSVectorScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TSVectorScanner) + scanner := dst.(TSVectorScanner) if src == nil { return scanner.ScanTSVector(TSVector{}) } - rp := 0 + r := pgio.NewReader(src) - const ( - uint16Len = 2 - uint32Len = 4 - ) - - if len(src[rp:]) < uint32Len { - return fmt.Errorf("tsvector incomplete %v", src) - } - entryCount := int(int32(binary.BigEndian.Uint32(src[rp:]))) - rp += uint32Len - - if entryCount < 0 { - return fmt.Errorf("tsvector invalid lexeme count: %d", entryCount) - } - // Each lexeme carries at minimum a 1-byte NUL terminator and a 2-byte position count, so - // entryCount cannot exceed remaining/3. This bounds the up-front make() against a malicious - // server claiming a huge lexeme count in a small message. - if maxEntries := len(src[rp:]) / 3; entryCount > maxEntries { - return fmt.Errorf("tsvector invalid lexeme count %d for %d remaining bytes", entryCount, len(src[rp:])) + // Each lexeme carries at minimum a 1-byte NUL terminator and a 2-byte position count. This + // bounds the up-front make() against a malicious server claiming a huge lexeme count in a + // small message. + entryCount := r.Count(3) + if err := r.Err(); err != nil { + return fmt.Errorf("tsvector: %w", err) } var tsv TSVector @@ -219,41 +204,35 @@ func (scanPlanBinaryTSVectorToTSVectorScanner) Scan(src []byte, dst any) error { } for i := range entryCount { - nullIndex := bytes.IndexByte(src[rp:], 0x00) - if nullIndex == -1 { - return fmt.Errorf("invalid tsvector binary format: missing null terminator") - } - - lexeme := TSVectorLexeme{Word: string(src[rp : rp+nullIndex])} - rp += nullIndex + 1 // skip past null terminator + lexeme := TSVectorLexeme{Word: string(r.CString())} - // Read position count. - if len(src[rp:]) < uint16Len { - return fmt.Errorf("invalid tsvector binary format: incomplete position count") - } - - numPositions := int(binary.BigEndian.Uint16(src[rp:])) - rp += uint16Len - - // Read each packed position: weight (2 bits) | position (14 bits) - if len(src[rp:]) < numPositions*uint16Len { - return fmt.Errorf("invalid tsvector binary format: incomplete positions") + numPositions := int(r.Uint16()) + if err := r.Err(); err != nil { + return fmt.Errorf("invalid tsvector binary format: lexeme %d: %w", i, err) } + // Each packed position is weight (2 bits) | position (14 bits). numPositions came from a + // uint16, so it cannot ask for an unreasonable allocation here. if numPositions > 0 { lexeme.Positions = make([]TSVectorPosition, numPositions) for pos := range numPositions { - packed := binary.BigEndian.Uint16(src[rp:]) - rp += uint16Len + packed := r.Uint16() lexeme.Positions[pos] = TSVectorPosition{ Position: packed & 0x3FFF, Weight: tsvectorWeightFromBinary(packed >> 14), } } + if err := r.Err(); err != nil { + return fmt.Errorf("invalid tsvector binary format: lexeme %d positions: %w", i, err) + } } tsv.Lexemes[i] = lexeme } + + if err := r.Finish(); err != nil { + return fmt.Errorf("tsvector: %w", err) + } tsv.Valid = true return scanner.ScanTSVector(tsv) @@ -318,7 +297,7 @@ func (TSVectorCodec) PlanScan(m *Map, oid uint32, format int16, target any) Scan type scanPlanTextAnyToTSVectorScanner struct{} func (s scanPlanTextAnyToTSVectorScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TSVectorScanner) + scanner := dst.(TSVectorScanner) if src == nil { return scanner.ScanTSVector(TSVector{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/uint32.go b/vendor/github.com/jackc/pgx/v5/pgtype/uint32.go index e6d4b1c..d6eed52 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/uint32.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/uint32.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "encoding/json" "fmt" "math" @@ -280,12 +279,13 @@ func (scanPlanBinaryUint32ToUint32) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 4 { - return fmt.Errorf("invalid length for uint32: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("uint32: %w", err) } - p := (dst).(*uint32) - *p = binary.BigEndian.Uint32(src) + p := dst.(*uint32) + *p = raw return nil } @@ -293,7 +293,7 @@ func (scanPlanBinaryUint32ToUint32) Scan(src []byte, dst any) error { type scanPlanBinaryUint32ToUint32Scanner struct{} func (scanPlanBinaryUint32ToUint32Scanner) Scan(src []byte, dst any) error { - s, ok := (dst).(Uint32Scanner) + s, ok := dst.(Uint32Scanner) if !ok { return ErrScanTargetTypeChanged } @@ -302,19 +302,18 @@ func (scanPlanBinaryUint32ToUint32Scanner) Scan(src []byte, dst any) error { return s.ScanUint32(Uint32{}) } - if len(src) != 4 { - return fmt.Errorf("invalid length for uint32: %v", len(src)) + n, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("uint32: %w", err) } - n := binary.BigEndian.Uint32(src) - return s.ScanUint32(Uint32{Uint32: n, Valid: true}) } type scanPlanBinaryUint32ToTextScanner struct{} func (scanPlanBinaryUint32ToTextScanner) Scan(src []byte, dst any) error { - s, ok := (dst).(TextScanner) + s, ok := dst.(TextScanner) if !ok { return ErrScanTargetTypeChanged } @@ -323,18 +322,19 @@ func (scanPlanBinaryUint32ToTextScanner) Scan(src []byte, dst any) error { return s.ScanText(Text{}) } - if len(src) != 4 { - return fmt.Errorf("invalid length for uint32: %v", len(src)) + raw, err := pgio.Uint32Exact(src) + if err != nil { + return fmt.Errorf("uint32: %w", err) } - n := uint64(binary.BigEndian.Uint32(src)) + n := uint64(raw) return s.ScanText(Text{String: strconv.FormatUint(n, 10), Valid: true}) } type scanPlanTextAnyToUint32Scanner struct{} func (scanPlanTextAnyToUint32Scanner) Scan(src []byte, dst any) error { - s, ok := (dst).(Uint32Scanner) + s, ok := dst.(Uint32Scanner) if !ok { return ErrScanTargetTypeChanged } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/uint64.go b/vendor/github.com/jackc/pgx/v5/pgtype/uint64.go index fc407bd..76a2b66 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/uint64.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/uint64.go @@ -2,7 +2,6 @@ package pgtype import ( "database/sql/driver" - "encoding/binary" "fmt" "math" "strconv" @@ -251,12 +250,13 @@ func (scanPlanBinaryUint64ToUint64) Scan(src []byte, dst any) error { return fmt.Errorf("cannot scan NULL into %T", dst) } - if len(src) != 8 { - return fmt.Errorf("invalid length for uint64: %v", len(src)) + raw, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("uint64: %w", err) } - p := (dst).(*uint64) - *p = binary.BigEndian.Uint64(src) + p := dst.(*uint64) + *p = raw return nil } @@ -264,7 +264,7 @@ func (scanPlanBinaryUint64ToUint64) Scan(src []byte, dst any) error { type scanPlanBinaryUint64ToUint64Scanner struct{} func (scanPlanBinaryUint64ToUint64Scanner) Scan(src []byte, dst any) error { - s, ok := (dst).(Uint64Scanner) + s, ok := dst.(Uint64Scanner) if !ok { return ErrScanTargetTypeChanged } @@ -273,19 +273,18 @@ func (scanPlanBinaryUint64ToUint64Scanner) Scan(src []byte, dst any) error { return s.ScanUint64(Uint64{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for uint64: %v", len(src)) + n, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("uint64: %w", err) } - n := binary.BigEndian.Uint64(src) - return s.ScanUint64(Uint64{Uint64: n, Valid: true}) } type scanPlanBinaryUint64ToTextScanner struct{} func (scanPlanBinaryUint64ToTextScanner) Scan(src []byte, dst any) error { - s, ok := (dst).(TextScanner) + s, ok := dst.(TextScanner) if !ok { return ErrScanTargetTypeChanged } @@ -294,18 +293,18 @@ func (scanPlanBinaryUint64ToTextScanner) Scan(src []byte, dst any) error { return s.ScanText(Text{}) } - if len(src) != 8 { - return fmt.Errorf("invalid length for uint64: %v", len(src)) + n, err := pgio.Uint64Exact(src) + if err != nil { + return fmt.Errorf("uint64: %w", err) } - n := binary.BigEndian.Uint64(src) return s.ScanText(Text{String: strconv.FormatUint(n, 10), Valid: true}) } type scanPlanTextAnyToUint64Scanner struct{} func (scanPlanTextAnyToUint64Scanner) Scan(src []byte, dst any) error { - s, ok := (dst).(Uint64Scanner) + s, ok := dst.(Uint64Scanner) if !ok { return ErrScanTargetTypeChanged } diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/uuid.go b/vendor/github.com/jackc/pgx/v5/pgtype/uuid.go index 476889a..725defe 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/uuid.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/uuid.go @@ -211,7 +211,7 @@ func (UUIDCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan type scanPlanBinaryUUIDToUUIDScanner struct{} func (scanPlanBinaryUUIDToUUIDScanner) Scan(src []byte, dst any) error { - scanner := (dst).(UUIDScanner) + scanner := dst.(UUIDScanner) if src == nil { return scanner.ScanUUID(UUID{}) @@ -230,7 +230,7 @@ func (scanPlanBinaryUUIDToUUIDScanner) Scan(src []byte, dst any) error { type scanPlanBinaryUUIDToTextScanner struct{} func (scanPlanBinaryUUIDToTextScanner) Scan(src []byte, dst any) error { - scanner := (dst).(TextScanner) + scanner := dst.(TextScanner) if src == nil { return scanner.ScanText(Text{}) @@ -249,7 +249,7 @@ func (scanPlanBinaryUUIDToTextScanner) Scan(src []byte, dst any) error { type scanPlanTextAnyToUUIDScanner struct{} func (scanPlanTextAnyToUUIDScanner) Scan(src []byte, dst any) error { - scanner := (dst).(UUIDScanner) + scanner := dst.(UUIDScanner) if src == nil { return scanner.ScanUUID(UUID{}) diff --git a/vendor/github.com/jackc/pgx/v5/pgtype/xml.go b/vendor/github.com/jackc/pgx/v5/pgtype/xml.go index 66e6dff..7536e2b 100644 --- a/vendor/github.com/jackc/pgx/v5/pgtype/xml.go +++ b/vendor/github.com/jackc/pgx/v5/pgtype/xml.go @@ -104,23 +104,6 @@ func (c *XMLCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPl switch target.(type) { case *string: return scanPlanAnyToString{} - - case **string: - // This is to fix **string scanning. It seems wrong to special case **string, but it's not clear what a better - // solution would be. - // - // https://github.com/jackc/pgx/issues/1470 -- **string - // https://github.com/jackc/pgx/issues/1691 -- ** anything else - - if wrapperPlan, nextDst, ok := TryPointerPointerScanPlan(target); ok { - if nextPlan := m.planScan(oid, format, nextDst, 0); nextPlan != nil { - if _, failed := nextPlan.(*scanPlanFail); !failed { - wrapperPlan.SetNext(nextPlan) - return wrapperPlan - } - } - } - case *[]byte: return scanPlanXMLToByteSlice{} case BytesScanner: @@ -133,6 +116,17 @@ func (c *XMLCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPl return &scanPlanSQLScanner{formatCode: format} } + // Map.planScan only tries the wrap scan plan funcs when the codec returns nil, and the fallback below is never nil. + // So explicitly return nil for a pointer to a pointer to let TryPointerPointerScanPlan handle it. It sets the target + // to nil for SQL NULL and otherwise allocates through any amount of pointer indirection before scanning. + // + // https://github.com/jackc/pgx/issues/1470 -- **string + // https://github.com/jackc/pgx/issues/1691 -- ** anything else + if targetType := reflect.TypeOf(target); targetType != nil && targetType.Kind() == reflect.Pointer && + targetType.Elem().Kind() == reflect.Pointer { + return nil + } + return &scanPlanXMLToXMLUnmarshal{ unmarshal: c.Unmarshal, } diff --git a/vendor/github.com/jackc/pgx/v5/pgxpool/pool.go b/vendor/github.com/jackc/pgx/v5/pgxpool/pool.go index 5c740b1..bcf7bfe 100644 --- a/vendor/github.com/jackc/pgx/v5/pgxpool/pool.go +++ b/vendor/github.com/jackc/pgx/v5/pgxpool/pool.go @@ -22,6 +22,7 @@ var ( defaultMaxConnLifetime = time.Hour defaultMaxConnIdleTime = time.Minute * 30 defaultHealthCheckPeriod = time.Minute + defaultPingTimeout = time.Duration(0) ) type connResource struct { @@ -342,20 +343,22 @@ func NewWithConfig(ctx context.Context, config *Config) (*Pool, error) { // ParseConfig builds a Config from connString. It parses connString with the same behavior as [pgx.ParseConfig] with the // addition of the following variables: // -// - pool_max_conns: integer greater than 0 (default 4) +// - pool_max_conns: integer greater than 0 (default is the greater of 4 or runtime.NumCPU()) // - pool_min_conns: integer 0 or greater (default 0) +// - pool_min_idle_conns: integer 0 or greater (default 0) // - pool_max_conn_lifetime: duration string (default 1 hour) // - pool_max_conn_idle_time: duration string (default 30 minutes) // - pool_health_check_period: duration string (default 1 minute) // - pool_max_conn_lifetime_jitter: duration string (default 0) +// - pool_ping_timeout: duration string (default 0, meaning no timeout) // // See Config for definitions of these arguments. // // # Example Keyword/Value -// user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-ca pool_max_conns=10 pool_max_conn_lifetime=1h30m +// user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-full pool_max_conns=10 pool_max_conn_lifetime=1h30m // // # Example URL -// postgres://jack:secret@pg.example.com:5432/mydb?sslmode=verify-ca&pool_max_conns=10&pool_max_conn_lifetime=1h30m +// postgres://jack:secret@pg.example.com:5432/mydb?sslmode=verify-full&pool_max_conns=10&pool_max_conn_lifetime=1h30m func ParseConfig(connString string) (*Config, error) { connConfig, err := pgx.ParseConfig(connString) if err != nil { @@ -448,6 +451,17 @@ func ParseConfig(connString string) (*Config, error) { config.MaxConnLifetimeJitter = d } + if s, ok := config.ConnConfig.Config.RuntimeParams["pool_ping_timeout"]; ok { + delete(connConfig.Config.RuntimeParams, "pool_ping_timeout") + d, err := time.ParseDuration(s) + if err != nil { + return nil, pgconn.NewParseConfigError(connString, "cannot parse pool_ping_timeout", err) + } + config.PingTimeout = d + } else { + config.PingTimeout = defaultPingTimeout + } + return config, nil } @@ -461,6 +475,9 @@ func (p *Pool) Close() { } func (p *Pool) isExpired(res *puddle.Resource[*connResource]) bool { + if p.maxConnLifetime <= 0 { + return false + } return time.Now().After(res.Value().maxAgeTime) } diff --git a/vendor/github.com/jackc/pgx/v5/pgxpool/rows.go b/vendor/github.com/jackc/pgx/v5/pgxpool/rows.go index f834b7e..8425116 100644 --- a/vendor/github.com/jackc/pgx/v5/pgxpool/rows.go +++ b/vendor/github.com/jackc/pgx/v5/pgxpool/rows.go @@ -3,6 +3,7 @@ package pgxpool import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) type errRows struct { @@ -18,6 +19,7 @@ func (e errRows) Scan(dest ...any) error { return e.err } func (e errRows) Values() ([]any, error) { return nil, e.err } func (e errRows) RawValues() [][]byte { return nil } func (e errRows) Conn() *pgx.Conn { return nil } +func (e errRows) TypeMap() *pgtype.Map { return nil } type errRow struct { err error @@ -90,6 +92,10 @@ func (rows *poolRows) Conn() *pgx.Conn { return rows.r.Conn() } +func (rows *poolRows) TypeMap() *pgtype.Map { + return rows.r.TypeMap() +} + type poolRow struct { r pgx.Row c *Conn diff --git a/vendor/github.com/jackc/pgx/v5/port-tamer.toml b/vendor/github.com/jackc/pgx/v5/port-tamer.toml new file mode 100644 index 0000000..2931519 --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/port-tamer.toml @@ -0,0 +1,70 @@ +# port-tamer — this project's TCP port allocation (https://github.com/jackc/port-tamer). +# +# WHY. pgx's test matrix is five PostgreSQL majors plus CockroachDB running at once. In the +# devcontainer every database service sat in the app container's network namespace, so each could +# own a well-known port (5414..5417, 5432, 26257) and a second devcontainer instance was simply a +# second namespace. Natively there is one network stack, and a git worktree is the equivalent of +# that second instance — two checkouts would collide on all six. +# +# port-tamer assigns each checkout one consecutive group of free ports and writes it to a dotenv +# file (.dev/ports.env — gitignored, per-checkout state). mise loads that file, which is how a +# plain shell, process-compose, and ./test.sh all see the same numbers. THIS file is the committed +# declaration of which ports a checkout needs; the allocation itself is not committed. +# +# The devcontainer uses it too. It has its own network namespace and so does not NEED distinct +# ports, but it runs the same per-checkout clusters, so the allocation is the single description +# of where this checkout's servers are, on either platform. +# +# mise run dev:ports show the allocation and whether each port is listening +# mise run dev:ports:ensure allocate on first use; idempotent afterwards +# mise run dev:ports:overwrite deliberately move to a different group (stop services first) +# +# A listening port does NOT move an existing allocation — it may well belong to this checkout's +# own running services. Moving is always the explicit `--overwrite` above. +# +# APPEND new entries at the END. Ports are assigned consecutively in the order below, so inserting +# or reordering renumbers the existing ones — which invalidates every saved allocation and forces +# each checkout to overwrite. Appending is compatible and costs nothing. + +version = 1 + +# The default range (10000..44999) overlaps the ephemeral ports Linux hands out for client sockets +# (32768..60999), so the kernel could occupy a port inside an allocated group while its service is +# down and then break the restart. This range clears both platforms' ephemeral ranges (macOS +# starts at 49152). +[allocation] +minimum = 16000 +maximum = 31000 +bind_address = "127.0.0.1" + +# One PostgreSQL cluster per major (scripts/devdb.rb). All five share one Unix socket directory — +# sockets are named .s.PGSQL., so the port is what distinguishes them, exactly as the +# devcontainer's shared /var/run/postgresql volume worked. +[[ports]] +name = "PGPORT_14" + +[[ports]] +name = "PGPORT_15" + +[[ports]] +name = "PGPORT_16" + +[[ports]] +name = "PGPORT_17" + +[[ports]] +name = "PGPORT_18" + +# CockroachDB (scripts/devcrdb.rb): SQL and the admin HTTP UI. The HTTP port must be allocated +# even though no test uses it — cockroach binds it unconditionally, and letting it pick its own +# default (8080) would collide between checkouts. +[[ports]] +name = "CRDB_PORT" + +[[ports]] +name = "CRDB_HTTP_PORT" + +# process-compose control API. The name is process-compose's own: every `process-compose ...` +# command in this checkout then targets THIS checkout's stack with no flags. +[[ports]] +name = "PC_PORT_NUM" diff --git a/vendor/github.com/jackc/pgx/v5/process-compose.yaml b/vendor/github.com/jackc/pgx/v5/process-compose.yaml new file mode 100644 index 0000000..8a1b18b --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/process-compose.yaml @@ -0,0 +1,189 @@ +# process-compose — the long-running half of this checkout's development environment. +# +# The division of labour (identical natively and in the devcontainer): +# +# mise one-shot work: bootstrap, ports, initdb, test, format +# process-compose long-running work: the five PostgreSQL servers and CockroachDB +# +# These are exactly the services .devcontainer/docker-compose.yml used to run as containers. They +# now run as ordinary processes against this checkout's own clusters under .dev, so the same +# command set works on macOS, on Linux, and inside the container. +# +# Note what this file does NOT contain: no ports, no resolved paths, no per-process environment +# blocks, no database bootstrap. Ports arrive through the environment (.dev/, exported by the +# launcher — scripts/dev.rb); data directories, binary locations and the socket directory are +# resolved once inside scripts/devdb.rb, so a server and its readiness probe cannot disagree; and +# one-shot work (initdb, creating pgx_test) stays in scripts this file merely sequences. +# +# Two kinds of environment variable ARE read here, both set by scripts/dev.rb and both with a +# default that makes a direct `process-compose up` start only PostgreSQL 18: +# +# PGX_LOGS_DIR DevPaths::LOGS_DIR — the layout belongs to that module, not to ten literals +# PGX_DISABLE_* "true" for an on-demand target, or for a PostgreSQL major whose binaries are +# missing. Disabled processes remain available for `db:start` and lazy tests. +# +# The five servers are identical apart from their major, so pg14 carries the YAML anchors and the +# rest alias them. Anchors are resolved by the YAML parser before process-compose sees the +# document, so a probe threshold or a shutdown signal is now one edit rather than five that can +# drift apart. +# +# mise run dev start PostgreSQL 18 and the supervisor +# mise run dev:all start every database +# mise run db:start pg16 crdb prewarm selected targets +# process-compose process list status, scriptable +# process-compose process logs pg16 logs for one server +# process-compose down stop this checkout's stack only +# +# Each checkout runs its own instance on its own control port (PC_PORT_NUM, from this checkout's +# allocation), which process-compose reads from the environment — so the commands above need no +# flags and never reach another checkout's stack. +version: "0.5" + +# A process whose dependency failed should not be reported as merely skipped, and shutdown should +# unwind in dependency order rather than all at once. +is_strict: true +ordered_shutdown: true + +processes: + # PostgreSQL 14, and the template for 15-18. `serve` initdb's the cluster if this checkout has + # none, then execs the server in the foreground — so a fresh checkout goes straight to + # `mise run dev` with no documented prerequisite step, and process-compose supervises the real + # postmaster. + pg14: &pg + disabled: ${PGX_DISABLE_PG14:-true} + command: ruby scripts/devdb.rb serve 14 + readiness_probe: &pg-probe + exec: + command: ruby scripts/devdb.rb ready 14 + initial_delay_seconds: 1 + period_seconds: 1 + failure_threshold: 30 + # SIGINT is PostgreSQL's fast shutdown: roll back open transactions and exit, rather than + # waiting for clients to disconnect. + shutdown: + signal: 2 + timeout_seconds: 20 + availability: + restart: on_failure + # Bounded: `on_failure` alone retries a PERMANENT error forever (a cluster built by the other + # platform, a corrupt PGDATA), burying the real message under a restart loop. Five attempts + # then stop, so the cause stays on screen. + max_restarts: 5 + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg14.log + + # Creates pgx_test with its extensions and its md5/scram/password/cert roles once the server is + # up, then exits. A no-op when the database already exists, so restarting the stack is free. + # + # This is a one-shot, and process-compose does not re-run one when its dependency restarts. + # On-demand starts therefore run the same idempotent setup through scripts/dev_services.rb. + # CockroachDB, whose store is in memory, creates its database from its readiness probe instead. + pg14-setup: &pg-setup + disabled: ${PGX_DISABLE_PG14:-true} + command: ruby scripts/devdb.rb setup 14 + depends_on: + pg14: + condition: process_healthy + availability: + restart: "no" + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg14-setup.log + + pg15: + <<: *pg + disabled: ${PGX_DISABLE_PG15:-true} + command: ruby scripts/devdb.rb serve 15 + readiness_probe: + <<: *pg-probe + exec: + command: ruby scripts/devdb.rb ready 15 + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg15.log + + pg15-setup: + <<: *pg-setup + disabled: ${PGX_DISABLE_PG15:-true} + command: ruby scripts/devdb.rb setup 15 + depends_on: + pg15: + condition: process_healthy + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg15-setup.log + + pg16: + <<: *pg + disabled: ${PGX_DISABLE_PG16:-true} + command: ruby scripts/devdb.rb serve 16 + readiness_probe: + <<: *pg-probe + exec: + command: ruby scripts/devdb.rb ready 16 + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg16.log + + pg16-setup: + <<: *pg-setup + disabled: ${PGX_DISABLE_PG16:-true} + command: ruby scripts/devdb.rb setup 16 + depends_on: + pg16: + condition: process_healthy + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg16-setup.log + + pg17: + <<: *pg + disabled: ${PGX_DISABLE_PG17:-true} + command: ruby scripts/devdb.rb serve 17 + readiness_probe: + <<: *pg-probe + exec: + command: ruby scripts/devdb.rb ready 17 + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg17.log + + pg17-setup: + <<: *pg-setup + disabled: ${PGX_DISABLE_PG17:-true} + command: ruby scripts/devdb.rb setup 17 + depends_on: + pg17: + condition: process_healthy + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg17-setup.log + + pg18: + <<: *pg + disabled: ${PGX_DISABLE_PG18:-false} + command: ruby scripts/devdb.rb serve 18 + readiness_probe: + <<: *pg-probe + exec: + command: ruby scripts/devdb.rb ready 18 + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg18.log + + pg18-setup: + <<: *pg-setup + disabled: ${PGX_DISABLE_PG18:-false} + command: ruby scripts/devdb.rb setup 18 + depends_on: + pg18: + condition: process_healthy + log_location: ${PGX_LOGS_DIR:-.dev/logs}/pg18-setup.log + + # CockroachDB, the `crdb` test target. Insecure, single-node and entirely in memory — the same + # configuration the cockroachdb/cockroach container ran with. + # + # It has NO setup process, deliberately. The store is in memory, so every restart of this node is + # an empty cluster, and a completed one-shot is not re-run when its dependency restarts: after a + # crash-restart the node reported Ready with no pgx_test in it and `./test.sh crdb` failed every + # test. `devcrdb.rb ready` therefore creates the database itself, which makes the probe true on + # the first start and on every restart alike. + crdb: + disabled: ${PGX_DISABLE_CRDB:-true} + command: ruby scripts/devcrdb.rb serve + readiness_probe: + exec: + command: ruby scripts/devcrdb.rb ready + initial_delay_seconds: 2 + period_seconds: 1 + failure_threshold: 45 + shutdown: + signal: 15 + timeout_seconds: 20 + availability: + restart: on_failure + max_restarts: 5 + log_location: ${PGX_LOGS_DIR:-.dev/logs}/crdb.log diff --git a/vendor/github.com/jackc/pgx/v5/rows.go b/vendor/github.com/jackc/pgx/v5/rows.go index 4e5cf95..33332dd 100644 --- a/vendor/github.com/jackc/pgx/v5/rows.go +++ b/vendor/github.com/jackc/pgx/v5/rows.go @@ -54,6 +54,9 @@ type Rows interface { // Scan reads the values from the current row into dest values positionally. dest can include pointers to core types, // values implementing the Scanner interface, and nil. nil will skip the value entirely. It is an error to call Scan // without first calling Next() and checking that it returned true. Rows is automatically closed upon error. + // + // As a special case, if dest is a single value implementing [RowScanner], the whole row is given to its ScanRow + // method instead of being scanned positionally. Scan(dest ...any) error // Values returns the decoded row values. As with Scan(), it is an error to @@ -68,6 +71,11 @@ type Rows interface { // Conn returns the underlying *Conn on which the query was executed. This may return nil if Rows did not come from a // *Conn (e.g. if it was created by RowsFromResultReader) Conn() *Conn + + // TypeMap returns the [pgtype.Map] the values of this Rows are decoded with. It is available even when [Rows.Conn] + // is nil, such as for a Rows created by [RowsFromResultReader]. It may return nil if the Rows carries no values, + // such as one representing only an error. + TypeMap() *pgtype.Map } // Row is a convenience wrapper over [Rows] that is returned by [Conn.QueryRow]. @@ -83,7 +91,21 @@ type Row interface { Scan(dest ...any) error } -// RowScanner scans an entire row at a time into the RowScanner. +// RowScanner scans an entire row at a time into the RowScanner. It is only used when it is the sole destination passed +// to [Rows.Scan] or [Row.Scan]. When passed alongside other destinations it is scanned as an ordinary single value. +// +// ScanRow always takes precedence over the destination's other scanning interfaces, such as +// [pgtype.CompositeIndexScanner]. A type implementing both must therefore dispatch within ScanRow, because the number of +// columns is not known until the row arrives: +// +// func (p *Person) ScanRow(rows pgx.Rows) error { +// if fds := rows.FieldDescriptions(); len(fds) == 1 { +// // Scan the single column via p's pgtype.CompositeIndexScanner implementation. rows.Scan(p) would +// // call ScanRow again. +// return rows.TypeMap().Scan(fds[0].DataTypeOID, fds[0].Format, rows.RawValues()[0], p) +// } +// return rows.Scan(&p.Name, &p.Age) +// } type RowScanner interface { // ScanRows scans the row. ScanRow(rows Rows) error @@ -336,6 +358,10 @@ func (rows *baseRows) RawValues() [][]byte { return rows.values } +func (rows *baseRows) TypeMap() *pgtype.Map { + return rows.typeMap +} + func (rows *baseRows) Conn() *Conn { return rows.conn } @@ -836,6 +862,13 @@ func fieldPosByName(fldDescs []pgconn.FieldDescription, field string, normalize if normalize { field = strings.ReplaceAll(field, "_", "") + } else { + // Explicit db tags can distinguish quoted identifiers that differ only by case. + for i, desc := range fldDescs { + if desc.Name == field { + return i + } + } } for i, desc := range fldDescs { if normalize { @@ -843,7 +876,7 @@ func fieldPosByName(fldDescs []pgconn.FieldDescription, field string, normalize return i } } else { - if desc.Name == field { + if strings.EqualFold(desc.Name, field) { return i } } diff --git a/vendor/github.com/jackc/pgx/v5/stdlib/sql.go b/vendor/github.com/jackc/pgx/v5/stdlib/sql.go index 576d3c6..fc8376d 100644 --- a/vendor/github.com/jackc/pgx/v5/stdlib/sql.go +++ b/vendor/github.com/jackc/pgx/v5/stdlib/sql.go @@ -57,12 +57,25 @@ // // # PostgreSQL Specific Data Types // -// The pgtype package provides support for PostgreSQL specific types. *pgtype.Map.SQLScanner is an adapter that makes -// these types usable as a sql.Scanner. +// As of Go 1.27, database/sql allows drivers to implement their own scanning logic by implementing the +// driver.RowsColumnScanner interface. This allows PostgreSQL types such as arrays to be scanned directly into Go +// values such as slices. +// +// var a []int64 +// err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(&a) +// +// In older versions of Go, *pgtype.Map.SQLScanner can be used as an adapter that makes these types usable as a +// sql.Scanner. // // m := pgtype.NewMap() // var a []int64 // err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(m.SQLScanner(&a)) +// +// The pgtype package provides support for PostgreSQL specific types. These types can be used directly in Go 1.27 and +// with *pgtype.Map.SQLScanner in older Go versions. +// +// var r pgtype.Range[pgtype.Int4] +// err := db.QueryRow("select int4range(1, 5)").Scan(&r) package stdlib import ( @@ -726,7 +739,9 @@ func (r *Rows) Close() error { return r.rows.Err() } -func (r *Rows) Next(dest []driver.Value) error { +// initValueFuncs prepares the database/sql representation of each column. Both +// Next and ScanColumn use these conversions so their driver.Values agree. +func (r *Rows) initValueFuncs() { m := r.conn.conn.TypeMap() fieldDescriptions := r.rows.FieldDescriptions() @@ -857,6 +872,10 @@ func (r *Rows) Next(dest []driver.Value) error { } } } +} + +func (r *Rows) Next(dest []driver.Value) error { + r.initValueFuncs() var more bool if r.skipNext { diff --git a/vendor/github.com/jackc/pgx/v5/stdlib/sql_go1.27.go b/vendor/github.com/jackc/pgx/v5/stdlib/sql_go1.27.go new file mode 100644 index 0000000..7faac04 --- /dev/null +++ b/vendor/github.com/jackc/pgx/v5/stdlib/sql_go1.27.go @@ -0,0 +1,115 @@ +//go:build go1.27 + +package stdlib + +import ( + "database/sql" + "database/sql/driver" + "errors" + "io" + "reflect" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +// Rows implements driver.RowsColumnScanner as of Go 1.27. +var _ driver.RowsColumnScanner = (*Rows)(nil) + +// NextRow implements the driver.RowsColumnScanner interface. It advances to the +// next row of data and returns io.EOF when there are no more rows. +func (r *Rows) NextRow() error { + var more bool + if r.skipNext { + more = r.skipNextMore + r.skipNext = false + } else { + more = r.rows.Next() + } + + if !more { + if err := r.rows.Err(); err != nil { + return err + } + return io.EOF + } + + return nil +} + +// ScanColumn implements the driver.RowsColumnScanner interface. It preserves +// database/sql conversions for scalar destinations and sql.Scanner implementations. +// Other destinations, such as Go slices, pgtype.Array, and pgtype.Range, are +// scanned directly using the pgx type map. +func (r *Rows) ScanColumn(scanCtx driver.ScanContext, index int, dest any) error { + if dest == nil { + return errors.New("destination not a pointer") + } + rv := reflect.ValueOf(dest) + if rv.Kind() == reflect.Pointer && rv.IsNil() { + return errors.New("destination pointer is nil") + } + + src := r.rows.RawValues()[index] + if isSQLScanDestination(rv.Type()) { + var value driver.Value + if src != nil { + r.initValueFuncs() + var err error + value, err = r.valueFuncs[index](src) + if err != nil { + return err + } + } + return sql.ConvertAssign(scanCtx, dest, value) + } + + m := r.conn.conn.TypeMap() + fd := r.rows.FieldDescriptions()[index] + return m.Scan(fd.DataTypeOID, fd.Format, src, dest) +} + +// isSQLScanDestination includes named scalar types and nullable pointers to +// database/sql destinations. Select the conversion before scanning: retrying a +// failed scan could call user code twice or discard its error. +func isSQLScanDestination(t reflect.Type) bool { + var namedPointers map[reflect.Type]bool + for { + if t.Implements(reflect.TypeFor[sql.Scanner]()) { + return true + } + // These interfaces distinguish native byte slices, such as + // FlatArray[byte] and PreallocBytes, from ordinary []byte destinations. + if t.Implements(reflect.TypeFor[pgtype.ArraySetter]()) || t.Implements(reflect.TypeFor[pgtype.BytesScanner]()) { + return false + } + if t.Kind() != reflect.Pointer { + break + } + // Only defined pointer types can form a cycle without reaching a + // non-pointer type. Leave those to pgx's bounded scan planning. + if t.Name() != "" { + if namedPointers[t] { + return false + } + if namedPointers == nil { + namedPointers = make(map[reflect.Type]bool) + } + namedPointers[t] = true + } + t = t.Elem() + } + + switch t.Kind() { + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, reflect.String, reflect.Interface: + return true + case reflect.Slice: + return t.Elem().Kind() == reflect.Uint8 + case reflect.Struct: + return t.ConvertibleTo(reflect.TypeFor[time.Time]()) + default: + return false + } +} diff --git a/vendor/github.com/jackc/pgx/v5/test.sh b/vendor/github.com/jackc/pgx/v5/test.sh index 8bab2d2..254217e 100644 --- a/vendor/github.com/jackc/pgx/v5/test.sh +++ b/vendor/github.com/jackc/pgx/v5/test.sh @@ -1,170 +1,35 @@ #!/usr/bin/env bash -set -euo pipefail - -# test.sh - Run pgx tests against specific database targets +# test.sh - Run the pgx test suite against a database target. # -# Usage: -# ./test.sh [target] [go test flags...] +# ./test.sh PostgreSQL 18 (default) +# ./test.sh pg14 PostgreSQL 14 +# ./test.sh crdb CockroachDB +# ./test.sh all every target, sequentially +# ./test.sh pg16 -run TestConnect trailing arguments are passed to `go test` # -# Targets: -# pg14 - PostgreSQL 14 (port 5414) -# pg15 - PostgreSQL 15 (port 5415) -# pg16 - PostgreSQL 16 (port 5416) -# pg17 - PostgreSQL 17 (port 5417) -# pg18 - PostgreSQL 18 (port 5432) [default] -# crdb - CockroachDB (port 26257) -# all - Run against all targets sequentially +# `mise run dev` starts PostgreSQL 18 and the supervisor. Other targets start for a test and stop +# afterwards unless they were explicitly prewarmed with `mise run db:start`. See DEVELOPMENT.md. # -# Examples: -# ./test.sh # Test against PG18 -# ./test.sh pg14 # Test against PG14 -# ./test.sh crdb # Test against CockroachDB -# ./test.sh all # Test against all targets -# ./test.sh pg16 -run TestConnect # Test specific test against PG16 -# ./test.sh pg18 -count=1 -v # Verbose, no cache, PG18 +# The logic lives in scripts/runtests.rb, which builds each target's PGX_TEST_* environment from +# scripts/lib/test_targets.rb — the one place those connection strings are defined. This wrapper +# exists so `./test.sh` keeps working; `mise run test` is equivalent. +set -euo pipefail -# Color output (disabled if not a terminal) -if [ -t 1 ]; then - GREEN='\033[0;32m' - RED='\033[0;31m' - BLUE='\033[0;34m' - NC='\033[0m' +root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +runtests="$root/scripts/runtests.rb" + +# `mise exec` is not only how this wrapper gets the pinned Ruby. It also puts process-compose, Go, +# and CockroachDB on PATH and loads this checkout's .dev/*.env files. Prefer it whenever it is +# available, including when the shell has a new-enough system Ruby but mise has not been activated. +# Invoking it from `mise run test` is harmless: this execs Ruby directly, so there is no task +# recursion. +if command -v mise > /dev/null 2>&1; then + exec mise exec -- ruby "$runtests" "$@" +elif ruby -e 'exit(RUBY_VERSION.split(".")[0].to_i >= 3 ? 0 : 1)' > /dev/null 2>&1; then + exec ruby "$runtests" "$@" else - GREEN='' - RED='' - BLUE='' - NC='' + echo "test.sh: needs Ruby 3.0 or newer (mise.toml pins one)." >&2 + echo " Install mise (https://mise.jdx.dev), then: mise install" >&2 + echo " See DEVELOPMENT.md." >&2 + exit 1 fi - -log_info() { echo -e "${BLUE}==> $*${NC}"; } -log_ok() { echo -e "${GREEN}==> $*${NC}"; } -log_err() { echo -e "${RED}==> $*${NC}" >&2; } - -# Wait for a database to accept connections -wait_for_ready() { - local connstr="$1" - local label="$2" - local max_attempts=30 - local attempt=0 - - log_info "Waiting for $label to be ready..." - while ! psql "$connstr" -c "SELECT 1" > /dev/null 2>&1; do - attempt=$((attempt + 1)) - if [ "$attempt" -ge "$max_attempts" ]; then - log_err "$label did not become ready after $max_attempts attempts" - return 1 - fi - sleep 1 - done - log_ok "$label is ready" -} - -# Directory containing this script (used to locate testsetup/) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CERTS_DIR="$SCRIPT_DIR/testsetup/certs" - -# Copy client certificates to /tmp for TLS tests -setup_client_certs() { - if [ -d "$CERTS_DIR" ]; then - base64 -d "$CERTS_DIR/ca.pem.b64" > /tmp/ca.pem - base64 -d "$CERTS_DIR/pgx_sslcert.crt.b64" > /tmp/pgx_sslcert.crt - base64 -d "$CERTS_DIR/pgx_sslcert.key.b64" > /tmp/pgx_sslcert.key - fi -} - -# Initialize CockroachDB (create database if not exists) -init_crdb() { - local connstr="postgresql://root@localhost:26257/?sslmode=disable" - wait_for_ready "$connstr" "CockroachDB" - log_info "Ensuring pgx_test database exists on CockroachDB..." - psql "$connstr" -c "CREATE DATABASE IF NOT EXISTS pgx_test" 2>/dev/null || true -} - -# Run tests against a single target -run_tests() { - local target="$1" - shift - local extra_args=("$@") - - local label="" - local port="" - - case "$target" in - pg14) label="PostgreSQL 14"; port=5414 ;; - pg15) label="PostgreSQL 15"; port=5415 ;; - pg16) label="PostgreSQL 16"; port=5416 ;; - pg17) label="PostgreSQL 17"; port=5417 ;; - pg18) label="PostgreSQL 18"; port=5432 ;; - crdb) - label="CockroachDB (port 26257)" - init_crdb - log_info "Testing against $label" - if ! PGX_TEST_DATABASE="postgresql://root@localhost:26257/pgx_test?sslmode=disable&experimental_enable_temp_tables=on" \ - go test -count=1 "${extra_args[@]}" ./...; then - log_err "Tests FAILED against $label" - return 1 - fi - log_ok "Tests passed against $label" - return 0 - ;; - *) - log_err "Unknown target: $target" - log_err "Valid targets: pg14, pg15, pg16, pg17, pg18, crdb, all" - return 1 - ;; - esac - - setup_client_certs - - log_info "Testing against $label (port $port)" - if ! PGX_TEST_DATABASE="host=localhost port=$port user=postgres password=postgres dbname=pgx_test" \ - PGX_TEST_UNIX_SOCKET_CONN_STRING="host=/var/run/postgresql port=$port user=postgres dbname=pgx_test" \ - PGX_TEST_TCP_CONN_STRING="host=127.0.0.1 port=$port user=pgx_md5 password=secret dbname=pgx_test" \ - PGX_TEST_MD5_PASSWORD_CONN_STRING="host=127.0.0.1 port=$port user=pgx_md5 password=secret dbname=pgx_test" \ - PGX_TEST_SCRAM_PASSWORD_CONN_STRING="host=127.0.0.1 port=$port user=pgx_scram password=secret dbname=pgx_test channel_binding=disable" \ - PGX_TEST_SCRAM_PLUS_CONN_STRING="host=localhost port=$port user=pgx_ssl password=secret sslmode=verify-full sslrootcert=/tmp/ca.pem dbname=pgx_test channel_binding=require" \ - PGX_TEST_PLAIN_PASSWORD_CONN_STRING="host=127.0.0.1 port=$port user=pgx_pw password=secret dbname=pgx_test" \ - PGX_TEST_TLS_CONN_STRING="host=localhost port=$port user=pgx_ssl password=secret sslmode=verify-full sslrootcert=/tmp/ca.pem dbname=pgx_test channel_binding=disable" \ - PGX_TEST_TLS_CLIENT_CONN_STRING="host=localhost port=$port user=pgx_sslcert sslmode=verify-full sslrootcert=/tmp/ca.pem sslcert=/tmp/pgx_sslcert.crt sslkey=/tmp/pgx_sslcert.key dbname=pgx_test" \ - PGX_SSL_PASSWORD=certpw \ - go test -count=1 "${extra_args[@]}" ./...; then - log_err "Tests FAILED against $label" - return 1 - fi - log_ok "Tests passed against $label" -} - -# Main -main() { - local target="${1:-pg18}" - - if [ "$target" = "all" ]; then - shift || true - local targets=(pg14 pg15 pg16 pg17 pg18 crdb) - local failed=() - - for t in "${targets[@]}"; do - echo "" - log_info "==========================================" - log_info "Target: $t" - log_info "==========================================" - if ! run_tests "$t" "$@"; then - failed+=("$t") - log_err "FAILED: $t" - fi - done - - echo "" - if [ ${#failed[@]} -gt 0 ]; then - log_err "Failed targets: ${failed[*]}" - return 1 - else - log_ok "All targets passed" - fi - else - shift || true - run_tests "$target" "$@" - fi -} - -main "$@" diff --git a/vendor/github.com/jackc/pgx/v5/tx.go b/vendor/github.com/jackc/pgx/v5/tx.go index 3f93a6f..dcb0feb 100644 --- a/vendor/github.com/jackc/pgx/v5/tx.go +++ b/vendor/github.com/jackc/pgx/v5/tx.go @@ -100,9 +100,12 @@ func (c *Conn) Begin(ctx context.Context) (Tx, error) { func (c *Conn) BeginTx(ctx context.Context, txOptions TxOptions) (Tx, error) { _, err := c.Exec(ctx, txOptions.beginSQL()) if err != nil { - // begin should never fail unless there is an underlying connection issue or - // a context timeout. In either case, the connection is possibly broken. - c.die() + // begin kills the connection upon receiving fatal, panic or non PGError errors, + // but does not otherwise as the connection should be reusable. + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) || isConnectionFatal(pgErr) { + c.die() + } return nil, err } @@ -112,6 +115,14 @@ func (c *Conn) BeginTx(ctx context.Context, txOptions TxOptions) (Tx, error) { }, nil } +func isConnectionFatal(pgErr *pgconn.PgError) bool { + severity := pgErr.SeverityUnlocalized + if severity == "" { + severity = pgErr.Severity + } + return severity == "FATAL" || severity == "PANIC" +} + // Tx represents a database transaction. // // Tx is an interface instead of a struct to enable connection pools to be implemented without relying on internal pgx diff --git a/vendor/modules.txt b/vendor/modules.txt index 0175a15..bd083d8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,10 +4,11 @@ github.com/jackc/pgpassfile # github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 ## explicit; go 1.14 github.com/jackc/pgservicefile -# github.com/jackc/pgx/v5 v5.10.0 +# github.com/jackc/pgx/v5 v5.11.0 ## explicit; go 1.25.0 github.com/jackc/pgx/v5 github.com/jackc/pgx/v5/internal/iobufpool +github.com/jackc/pgx/v5/internal/pgdatetime github.com/jackc/pgx/v5/internal/pgio github.com/jackc/pgx/v5/internal/sanitize github.com/jackc/pgx/v5/internal/stmtcache