Skip to content

Changelog

[3.1.0] - 06.10.2026

⚠ BREAKING CHANGES

  • [GL-82] feat(async)!: wrap prepared statements. Connection.prepare() now returns a Picopyn prepared statement wrapper that preserves asyncpg's execution API while keeping Picopyn error reporting and stale-plan handling intact.

BREAKING CHANGE: picopyn.asynchronous.Connection.prepare() now returns picopyn.asynchronous.PreparedStatement instead of asyncpg.prepared_stmt.PreparedStatement.

Features

  • [GL-108] feat: support runtime pool settings updates.

  • [GL-107] feat(async): add default close timeout.

  • [GL-110] feat: support runtime topology and metadata disabling.

  • [GL-115] feat: close excess free connections after max_size decrease. Decreasing max_size closed excess connections only once they were acquired and released, so an idle pool kept them open.

Now any max_size change triggers a topology refresh, and rebalance first trims the pool down to max_size, taking connections from the most loaded nodes.

  • [GL-82] feat: temporarily eject instances from pool selection after connection failures. Track connection failures per Picodata instance and temporarily exclude ejected instances from automatic and shard-aware Pool selection.

  • [GL-83] feat: filter pool connections by instance role.

Docs

  • update docs for 3.1 release. Describe instance role filtering, instance ejection and runtime pool settings in README and docs index, clarify multi-host DSN examples for bootstrap and discovery modes, and state the default close() timeout.

Move InstanceRole to the public picopyn.instance_role module and add Pool.instance_ejection_enabled, which now holds the instance ejection documentation that other docstrings link to.

More

  • test: run ci against picodata 25.4.4.

[3.0.0] - 11.09.2026

⚠ BREAKING CHANGES

  • [GL-102] feat!: remove client and async pool.connect(). Client was a thin wrapper over Pool with no added value, deprecated in 2.1. Both the sync and the async Client are now removed -- use Pool instead. The deprecated async Pool.connect() alias is removed as well; use Pool.open():
  # picopyn < 3.0.0
  from picopyn.asynchronous import Client as AsyncClient
  client = AsyncClient(dsn, pool_size=10)
  await client.connect()

  from picopyn.synchronous import Client as SyncClient
  client = SyncClient(dsn, pool_size=10)
  client.connect()

  # picopyn >= 3.0.0
  from picopyn.asynchronous import Pool as AsyncPool
  pool = AsyncPool(dsn, max_size=10, enable_discovery=True)
  await pool.open()

  from picopyn.synchronous import Pool as SyncPool
  pool = SyncPool(dsn, max_size=10, enable_discovery=True)
  pool.open()

Note: client's pool_size is pool's max_size. Also, for client the discovery mode was always enabled, while in the pool you need to pass enable_discovery=True explicitly to keep that behaviour (Pool defaults to False).

Features

  • [GL-88] feat(sync): support topology view.

  • [GL-89] feat: collect tiers and bucket map.

  • [GL-90] feat(sync): add to_evict processing.

  • [GL-104] feat(async): allow disabling the query-metadata service. The pool always created the query-metadata service, so every pool opened a technical connection to Picodata and kept a metadata cache even when shard-aware routing was not needed at all.

Now query_metadata_cache_size=None turns the service off: no cache, no technical connection. Also, the pool rejects a size less than 1 up front.

  • [GL-91] feat(sync): reconcile pool membership with topology changes.

  • [GL-92] feat(sync): cache picodata's insert-query sharding meta.

  • [GL-93] feat(sync): support bucket_id to replicaset master mapping.

  • [GL-106] feat(sync): support pool graceful shutdown. Before, close() closed the free connections and returned right away, leaving the used ones untouched (considered they will be closed later on release). The caller had no way to wait for them, and no way to close them at all if their owners never came back.

Now close() waits for the used connections too, bounded by timeout (1 hour by default), after which the rest are closed anyway; the same deadline bounds stopping the topology tracker. Concurrent close() calls join the same shutdown. The new terminate() closes everything without waiting and finishes a running close(); the topology tracker and the query-metadata service got terminate() as well, so shutdown never joins a refresh or a prepare stuck on a query -- the new Connection.cancel() interrupts it instead.

  • [GL-94] feat(sync): route execute by query metadata.

  • [GL-95] feat: add sharding key routing api.

  • make the routing wait for a bucket master connection tunable. Shard-aware routing asked for a connection to the bucket master with a zero timeout and fell back to an ordinary pool connection the moment none was free. Introduce new pool setting routing_acquire_timeout.

Also routing no longer waits at all when the pool holds no connection to the bucket master, neither free nor in use.

Fixed

  • bound background query-metadata prepares. Every metadata-cache miss started its own background prepare, repeated misses on the same query included. Under a stream of misses the tasks piled up faster than the single dedicated connection could drain them, and a query Picodata never answers with a metadata Notice was asked about again on every miss.

The service now tracks the queries a prepare is already running for, so a second miss joins the running one instead of starting another, and it stops creating prepares at all once cache_size of them are pending.

Also if Picodata did not accept the Parse (for example because of Picodata 25.5.9) we do not expect metadata Notice response and do not ask Parse the query again. If picodata accepted the Parse (sent ParseComplete) but sent no metadata Notice, we also cache it.

  • [GL-113] fix(sync): prepare pool statements on first execution by default. psycopg prepares a statement only after prepare_threshold executions (5 by default). Until then Picodata cannot raise its stale-plan signal for the query, so after DDL the pool kept routing by the cached query meta that was no longer valid.

Pool connections are now opened with prepare_threshold=0 unless the caller passes prepare_threshold in the pool connect kwargs. Technical connections keep the kwargs as given.

Changes

  • move shared pool node-choice logic to utils. The sync/async Pool kept their own copies of the same reconcile parts that were not actually depends on sync/async.

Node choice now lives in the new module utils/balance.py as pure functions over a snapshot of the pool's connections. Discovery row collection and some topology tracker logic also moved to its utils.

  • key the query metadata cache by query text. The cache hashed every query with SHA-256 and used the hex digest as its key. But the key was the same string object already held in prepared statement metadata, so every lookup paid for a hash (on top of the hashing dicts does anyway)

Docs

  • pin the pypi badge to the built version. The badge pulled the current version from shields.io, so the docs of an older release still advertised the latest version published to PyPI. Replace it with a static badge carrying a $VERSION$ placeholder and add an MkDocs hook that substitutes the installed picopyn version at build time.

  • anchor changelog issue links and commit type. git-cliff matched GL-NN anywhere in the commit message, so an issue mentioned in the body was rendered as a second [[GL-N](...)] prefix and broke the cross-references. Anchor the pattern at the start of the message, so only the issue from the commit subject becomes a prefix link.

The commit type was matched the same way, for example, deps!: bump minimum supported picodata version landed in Docs because its body links to docs.picodata.io. Anchor the type patterns as well, allowing only a GL-NN: prefix before them.

  • update docs for 3.0 release. Extend the sync docstrings with the pool description, connection and SSL examples, and the diagram snippets already included in the async pool.

The snippets are now shared by both APIs, so their click-anchors contain a asynchronous placeholder that a new hook expands to either sync or async depending on the page.

  • deduplicate breaking change changelog-section.

  • keep commit type in more changelog section.

More

  • [GL-72] test: gate integration tests by picodata version. Some driver features are supported only since a specific Picodata version. They do not have to work with older Picodata but must not break functionality such as opening the pool, executing queries etc. Previously we did not have a test instrument to check it.

So now we can mark feature-tests with requires_picodata (they will be run only with Picodata with version >= than required for feature) and mark degradation tests with without_feature (they assert that base functionality works even if the specific feature does not). For example: shard-aware routing works with Picodata >= 26.1.1 for INSERT queries, but with older Picodata the driver still can execute INSERT queries (just less effectively).

Added Picodata version matrix to CI: run tests against both 25.5.9 and 26.1.6.

  • test: split unit tests into sync/async.

  • perf(async): wake acquire waiters as soon as a connection is free. Acquire loop polled the pool with asyncio.sleep(0.1), so a caller waiting for a connection noticed a released one only on its next poll and could wait for up to 100 ms even though the pool already had it free.

The pool lock now carries two asyncio conditions: every path that puts a connection back into _pool (release/rebalance/..) wakes the waiters, and closing the pool wakes them too, so they stop waiting.

Also waiters are split in two: waiters for specific node's connection and waiters for any any available connection.

  • perf: bisect the bucket ranges. Bucket map search lookup runs on every shard-aware execute, and it scanned the whole map on each call until hit the needed bucket range. So now leep a list of the sorted range and bisect it instead: ranges within a tier never overlap, so the last range startig <= bucket_id is the only candidate and just its end has to be checked. The similar problem solved with applying bucket events

  • perf: log topology events with %-style formatting. The topology event handlers built their debug messages with f-strings, so the event repr was rendered on every applied event even with DEBUG off. Pass the values as logging arguments instead, so the formatting happens only when the record is actually emitted.

  • test: add integration test on meta cache invalidation.

[2.1.0] - 26.08.2026

Features

  • [GL-85] feat(async): reconcile pool membership in bootstrap mode. Previously reconcile was implemented for the pool only in discovery mode, so a bootstrap-mode pool never followed the cluster: the dead nodes could stay in the pool and if they left the pool was never refilled.

Now the reconcile-callback is always registered when topology tracking is on, and enable_discovery only decides which nodes are candidates: the whole cluster (discovery) or the dsn nodes only (bootstrap).

  • [GL-96] feat(async): route technical connections to the least loaded node. Technical connections (currently the topology tracker and the query metadata service) always went to the first reachable DSN host, so every client sharing a DSN piled onto the same node, and a bootstrap-mode pool could not reach the rest of the cluster at all.

Now such a connection is opened from the topology snapshot: - pick the least loaded online allowed-tier node; - if there are none, pick the least loaded forbidden-tier node -- otherwise, once every allowed tier went offline, the tracker would have nowhere to connect and the pool could never learn that those tiers are back; - if there is no topology yet, fall back to the DSN list.

Load is counted as the number of user connections (free and used) the pool holds to that node; technical connections themselves are not counted. Nodes with equal (tier, count) are ordered randomly, so clients sharing the same Picodata cluster do not all pick the same node.

  • [GL-96] feat(async): rebalance a full pool across the nodes. Reconcile only evicted connections to dead nodes and refilled the pool up to max_size, so a full pool was never touched again. A node that returned to Online (or joined the cluster) got no connections at all, and the skew lasted until one of the pooled nodes died:

nodes=[i1 i2 i3], max_size=6 => pool=[i1 i2 i3 i1 i2 i3] i3 goes Offline => pool=[i1 i2 i1 i2 i1 i2] i3 is back Online => pool stays 3/3/0, refill does nothing

Now reconcile ends with a rebalance pass: it counts free and in-use connections per candidate and migrates one connection from the most loaded node to the least loaded one.

One rebalance loop does a limited number of migrations, and leaves the res t to the next refresh, so a topology event does not trigger too many reconnections at once.

  • [GL-102] feat: deprecate client and async pool.connect(). Client is a thin wrapper over Pool that adds nothing on top of it, so both the sync and the async Client now emit a DeprecationWarning and will be removed in the next major release. Use Pool instead.

The async Pool called its initialization method connect(), while the sync Pool already had open(). Also, in asyncpg terms connect() returns a single connection to one node. So to avoid misunderstanding, introduce Pool.open() to the async pool and keep connect() as a deprecated alias until next major.

Fixed

  • [GL-98] fix(async): keep dsn options when adding picodata startup options. After GL-64 connect() always passes its own server_settings["options"] (with specific Picodata options). Asyncpg resolves a conflict with the DSN in favor of connect_kwargs, so ?options= from the DSN was silently dropped.

So, to keep the caller's options: - if connect_kwargs already sets server_settings["options"], the Picodata options are appended to it -- the DSN options would be discarded by asyncpg anyway; - otherwise the value is taken from the DSN, so it survives the keyword argument this function builds; - the Picodata options are appended last on purpose: the last value of a repeated option wins, so out pico options will be kept even if the caller tried to turn it off.

  • [GL-100] fix: count in-use pool connections against max_size. connect and both pool-filling loops compared len(self._pool) with max_size, ignoring the connections already handed out to user

  • keep pool fill even when a node drops out. Both pool fill loops picked a node by index modulo the candidate list length and removed a node from that same list when it could not be used (connection refused, or rejected by forbidden_tiers). Removing an element made every following index point at a different node, so the remaining nodes got an uneven number of connections: input: nodes=[a,b,c,d], max_size=8 idx 0..5: connected to a b c d a b idx 6: c refuses => c is removed => nodes=[a,b,d], idx stays 6 idx 6: 6 % 3 = 0 => a again idx 7..8: b, d result node -> conn count: a->3, b->3, c->1, d->1 b node ends up with 3 connections while d has 1 and could accept more; the expected result is a->3, b->2, c->1, d->2.

Rotate a deque instead: a successful connect rotates the candidate to the back, an unusable node is popped from the front, so dropping a node no longer disturbs the order of the others.

  • [GL-103] fix: don't compute bucket_id for decimal values over 28 significant digits. Picodata's own limit is 38, but for bind parameter decimal values it is 28 (we bind parameters for rows with calculated bucket_id). Also there is a bug (see https://git.picodata.io/core/picodata/-/work_items/3121) that makes values overflow that limit incorrectly, so we reject anything above 28 digits here rather than risk computing a bucket_id that doesn't match the server's.

Changes

  • [GL-84] refactor: replace instance_uuid with pico_instance_uuid.

  • [GL-81] refactor: remove unused _fetch_current_instance_info.

Docs

  • document sync client/pool and add pypi links. - render sync Client and Pool by switching docs/sync.md to filters instead of an explicit members list
  • document the connect alias and the autocommit property
  • add PyPI badges, install link, social icon and nav entry

  • [GL-87] docs: explain module documentation.

  • actualize doc for 2.1 release.

[2.0.0] - 12.08.2026

⚠ BREAKING CHANGES

  • [GL-63] deps!: bump minimum supported picodata version to 25.5.1. We're adding bucket map collection to the topology, which relies on the _pico_bucket function, supported starting with Picodata 25.5.1: https://docs.picodata.io/picodata/devel/reference/sql/system_functions/#_pico_bucket

  • [GL-64] deps!: bump minimum supported picodata version to 26.1.1. We implemented the client side of Picodata's query metadata protocol https://git.picodata.io/core/picodata/-/blob/master/doc/adr/2025-09-12-pgproto-query-metadata.md

The protocol is available starting with Picodata 26.1.1.

Features

  • [GL-59] feat(sync): support client.

  • [GL-70] feat(sync): close connections via context manager.

  • [GL-63] feat(async): collect bucket map to topology. Topology now tracks bucket distribution: bucket ranges are polled via Picodata's _pico_bucket(tier) scalar function: https://docs.picodata.io/picodata/devel/reference/sql/system_functions/#_pico_bucket Its result converted into BucketEvents applied the same way as instance/replicaset events, per the topology notifications ADR: https://git.picodata.io/core/picodata/-/blob/master/doc/adr/2025-09-12-pgproto-topology-notifications.md

  • [GL-66] feat: support uuid field in connection.

  • [GL-64] feat(async): cache picodata's insert-query sharding meta. Implements the client side of Picodata's query metadata protocol https://git.picodata.io/core/picodata/-/blob/master/doc/adr/2025-09-12-pgproto-query-metadata.md

A dedicated connection (QueryMetadataService) requests query disctribution meta and keeps a pool-level cache, invalidated on Picodata's stale-plan signal (SQLSTATE 42999). Currently meta is only ever populated for INSERT (https://git.picodata.io/core/picodata/-/issues/2226)

Also adds a public Connection.prepare(), relying on asyncpg's own built-in statement cache -- a separate concern from the meta-cache introduced here.

bucket_id computation/routing based on is future work.

  • [GL-68] feat(async): add to_evict processing.

  • [GL-65] feat: support calculate_bucket_id function. Implement picopyn.bucket.calculate_bucket_id that can calculate query's bucket_id by its metadata (tier name and param-names) and bound parameters (specific values). MessagePack-encode each distribution-key parameter, concatenate, hash with MurmurHash, then reduce mod bucket_count if target tier.

Support the scalar SQL types Picodata (int/double/datetime/etc) and its combinations (composite sharding key) as a distribution key.

New function will allow us to route queries to specific Picodata instance

Based on the Picodata bucket_id calculation algorithm: https://git.picodata.io/core/picodata/-/blob/master/doc/adr/2025-09-12-pgproto-query-metadata.md#алгоритм-вычисления-bucket_id

  • [GL-67] feat(async): support bucket_id to replicaset master mapping.

  • [GL-77] feat(async): support pool graceful shutdown.

  • [GL-42] feat(async): reconcile pool membership with topology changes. When discovery mode enabled, wire Pool reconcil membership as the topology tracker's on_refresh callback, so the pool's connections now follow the cluster instead of being decided once at connect() time.

With discovery disabled, tracking stays purely observational as before.

  • [GL-8] feat(async): route execute by query metadata.

Fixed

  • [GL-71] fix: add missed uv.lock to release commit.

  • [GL-42] fix(async): collect tier names and bucket counts into topology. Previously, we did not collect the tier_name -> bucket_count mapping.

The Picodata notification mechanism would let us calculate this after receiving the ReadyForQuery signal, but Picodata does not send that notification yet. So instead, we simply collect tier info from _pico_tier once and store it in the topology.

  • [GL-79] fix(async): forward pool topology settings to client.

  • [GL-80] fix(sync): support autocommit property.

Changes

  • [GL-75] refactor: optimize dsn tier lookup.

Docs

  • [GL-78] docs: actualize doc for 2.0 release. - move mermaid diagrams from dostring to docs/ as external snippets
  • actualize diagrams/dostring
  • fix outdated repo URL (solution/picopyn -> core/drivers/picopyn)
  • add changelog page and update version compatibility table

More

  • [GL-62] test: move to picodata 26.1.6.

  • [GL-76] test(async): close client/pool after test.

  • test: sync deps before test running.

  • style: set linters target-version same as requires-python (>=3.10). ruff/mypy target-version was set to py311, but the package's declared

    =3.10 minimum so

  • [GL-23] deps: relax dependency constraints. Exact pins (==) on packages like msgpack, mmh3 etc cause resolver conflicts whenever a application needs a different transitive version (e.g. Superset requires msgpack>=1.0.0,<1.1). Switch these to compatible ranges instead.

Also drop the direct async-timeout dependency: it's only needed by asyncpg on Python < 3.11, and asyncpg already declares it conditionally, so pinning it directly here was redundant.

psycopg and asyncpg remain pinned exactly.

[1.0.0] - 17.07.2026

⚠ BREAKING CHANGES

  • remove api import from picopyn root. Previously from picopyn import Client/Connection/Pool/... implicitly imported the async API, while sync always required an explicit picopyn.synchronous import. Now picopyn/init.py only sets up logging and re-exports nothing -- both APIs must be imported explicitly:

    from picopyn.asynchronous import Client from picopyn.synchronous import Connection

BREAKING CHANGE: from picopyn import Client (and Connection/Pool) no longer works. Use from picopyn.asynchronous import ... instead.

Features

  • [GL-31] feat: move release build to uv.

  • [GL-45] feat: add sync connect/close.

  • [GL-46] feat: add sync cursor.

  • [GL-47] feat(sync): support fetchmany and arraysize to cursor.

  • [GL-48] feat(sync): support execute and executemany.

  • [GL-38] feat(sync): support ssl.

  • [GL-40] feat(sync): support no-op transaction.

  • [GL-55] feat(sync): implement basic pool.

  • [GL-42] feat(async): support topology view. Track cluster topology (instances, replicasets) on the client, modeled after Picodata's topology notifications RFC: https://git.picodata.io/core/picodata/-/blob/master/doc/adr/2025-09-12-pgproto-topology-notifications.md

Picodata doesn't push topology changes as NOTIFY events yet (https://git.picodata.io/core/picodata/-/issues/1306), so for now the client polls Picodata's system tables directly, through a TopologySource abstraction. Once Picodata starts pushing real events, consider to replace topology source with listeting picodata notifications

Currently the pool only reads this topology -- it does not change its connection. Acting on topology changes (refilling pool connections, bucket-aware routing, etc.) is intentionally left for follow-up work.

  • [GL-56] feat(sync): acquire and release connections.

  • [GL-57] feat(sync): support pool execute, fetch, fetchrow.

  • [GL-58] feat(sync): support discovery and forbidden tiers.

Fixed

  • [GL-32] fix: change changelog filename.

  • clean up connections after pool initialization failure.

  • [GL-50] fix: hide dsn password from logs and errors. Previously, the full DSN, including the user's password, was included in error messages and debug/warning logs. While this was convenient during development, it was unsafe for production logs and error trackers.

Now the password is redacted as ***. For example:

(original) "postgresql://user:pass@host1:5432,host2:5432/db?sslpassword=keypass" (redacted) "postgresql://user:@host1:5432,host2:5432/db?sslpassword="

  • [GL-60] fix: remove closed connections after failed pool connect.

Changes

  • [GL-35] refactor: move shared code to utils.

  • [GL-37] refactor: extract async implementation into asynchronous package.

  • [GL-44] refactor: replace print statements with logging.

  • [GL-51] refactor(sync): move connect to factory method.

Docs

  • introduce new docs based on mkdocs.

  • [GL-53] docs: publish docs to readthedocs. Docs are published automatically to https://picopyn.readthedocs.io on each release tag via webhook integration with ReadTheDocs. .readthedocs.yaml is configuration file for doc building

  • [GL-53] docs: add missed contributing.md.

  • [GL-43] docs(async): add mermaid diagrams for pool init/usage flow.

  • add db-api docstring references. - link Connection/Cursor to the relevant PEP 249 sections

  • group sync error classes under a dedicated Errors heading instead of listing them alongside Connection/Cursor
  • add a usage example for Cursor.iter

  • add sync examples.

  • fix picodata compatibility info.

More

  • chrone: fix unreleased title in changelog.

  • test: fixed ssl cert files permissions for local test.

  • [GL-49] build: use uv instead of pip.

  • [GL-52] chore: add certs to repository. To simplify testing, SSL certificates have been added to the repository.

Closes GL-52

  • [GL-52] test: set ssl certificate files permissions. We now store SSL certificates in the repository, but Git does not allow us to set their permissions, which are needed to run the tests via Docker. Permission grants are now included in the Makefile.

  • [GL-61] deps: bump picodata version to 25.5.9.

  • chore: add breaking changes section to changelog.

[0.2.0] - 13.04.2026

Features

  • [GL-21] feat: add support of multiple hosts.

  • [GL-18] feat: add forbidden tiers list.

  • [GL-17] feat: add explain api.

  • [GL-20] feat: add ssl support.

Fixed

  • [GL-26] fix: add context manager support for pool.

Docs

  • добавлено больше примеров в документацию.

More

  • build(.gitlab-ci): setup publish stage.

  • [GL-22] chore: bump python version to 3.10.

  • ci: добавлен stage для тестирования релиза picodata.

  • ci: allow to redefine picodata_image variable for test-picodata job.

  • chrone: add make command to automate release commit.

[0.1.1] - 03.09.2025

Features

  • [GL-2] feat: implement connection pool.

  • [GL-4] feat: implement node discovery mode to connection pool.

  • [GL-5] feat: implement balance strategy for pool and client. - add client class

  • add custom load balance strategy for client and pool
  • use round-robin as default balance strategy

  • [GL-15] feat: added pool_size parameter for client.

  • [GL-16] feat(lint): introduce code linting and formatting. - added mypy, black, ruff as linters and formatters

  • actualize requirements.txt and requirements-dev.txt
  • applied formatting and type annotations across codebase (library + tests)
  • added separate GitLab CI job for linting

Fixed

  • [GL-14] fix: handle connection errors (e.g. connect, execute sql).

Docs

  • [GL-9] docs: add auto-generated api documentation from docstrings. - Setup pdoc toolchain for auto-documentation
  • Update README.md with new documentation workflow

More

  • [GL-12] test: bench the driver. - added bench scripts
  • added instructions
  • added example

  • [GL-11] драйвер подготовлен для публикации в pypi.

  • chore(.gitignore): add dist files.

  • build(pypi): remove setup.cfg in favor of pyproject.toml.