Use the protocol, replication, server, and SQL APIs

Add the protocol crate, connect a Flyology socket, and select a bounded client or server flow. Add a versioned SQL crate when the application must parse PostgreSQL syntax.

STEP 01

Prepare Flyology before adding the crate

Flyology Postgres is an Alire library crate. It uses Flyology task-aware sockets and depends on hmac_ada plus system_random for SCRAM-SHA-256.

Flyology
Use the runtime and toolchain preparation documented by the main project.
Postgres
Normal protocol interoperability is exercised with Postgres 18.4. Physical and logical replication are exercised against real Postgres 14 through 18 servers and recovery-mode standbys.
Transport
The included adapters support plaintext sockets, verified TLS upgrades, and accepted Flyology connections.
STEP 02

Add the crate to an Alire application

Configure the Flyology organization index, then let Alire resolve Flyology and Flyology Postgres as regular dependencies.

Flyology organization index
alr index --reset-community
alr index --add=git+https://github.com/flyology-ada/alire-index.git \
  --name=flyology --before=community
alr with flyology_postgres
STEP 03

Connect, verify TLS, and complete startup

Flyology.Postgres.Client provides the client session. Its Session borrows a transport, so the transport must outlive the session.

The Transports.TLS_Sockets adapter owns the socket after an upgrade. Its TLS_Socket_Transport implements the transport interface.

Startup_TLS requires TLS, verifies the certificate chain and DNS name, authenticates, and leaves the session ready for commands.

client startup
with Flyology.IO.Sockets;
with Flyology.IO.TLS.OpenSSL;
with Flyology.Postgres.Client;
with Flyology.Postgres.Transports.TLS_Sockets;

package Sockets renames Flyology.IO.Sockets;
package OpenSSL renames Flyology.IO.TLS.OpenSSL;
package Client renames Flyology.Postgres.Client;
package Transports renames Flyology.Postgres.Transports.TLS_Sockets;

Server  : constant Sockets.Endpoint :=
  Sockets.Network_Endpoint (Sockets.Loopback_IPv4, 5_432);
Backend : OpenSSL.OpenSSL_Provider;
Socket  : aliased Sockets.Socket_Type;
Channel : aliased Transports.TLS_Socket_Transport (Socket'Access);
Session : Client.Session (Channel'Access);

OpenSSL.Initialize_Client (Backend, CA_File => "root-ca.pem");
Sockets.Create_Socket (Socket, Family => Server.Family);
Sockets.Connect (Socket, Server, Timeout => 5.0);
Client.Startup_TLS
  (Session,
   Backend,
   Server_Name      => "db.example.com",
   User             => "app",
   Database         => "app",
   Password         => "secret",
   Application_Name => "flyology_app",
   Timeout          => 5.0);
STEP 04

Compose PostgreSQL work with other Flyology operations

The client operation overloads register limited operations in a caller-owned completion set. Use Connection_Transport for these overloads. The socket-specific transport adapters support only the synchronous APIs.

The connection transport borrows a Flyology connection and an optional cancellation token. The connection, transport, session, and completion set must outlive every operation that uses them.

Use the operation-producing Connections.Connect overload when connection setup must join the completion set. Declare the managed objects in ownership order.

operation-capable session objects
Manager    : aliased Connections.Server (Capacity => 1);
Connection : aliased Connections.Connection (Manager'Access);
Channel    : aliased Connection_Transports.Connection_Transport
  (Connection'Access, null);
Session    : aliased Client.Session (Channel'Access);
Set        : aliased Operations.Completion_Set (Capacity => 2);

declare
   Attempt : Connections.Connect_Operation :=
     Connections.Connect
       (Set'Access,
        Manager'Access,
        Database_Endpoint,
        Timeout => 5.0);
begin
   Operations.Wait_All (Set);
   Connections.Finish (Attempt, Connection);
end;

The managed connect temporarily uses two completion-set slots: one parent and one hidden socket operation. It retains the socket and admission permit until Finish transfers them to the closed connection.

Use the synchronous Connections.Connect procedure when connection setup does not need composition. Use Connections.Take when the application already owns a connected or specially configured socket.

Start
Send_Query returns a Send_Operation. The function overload of Receive_Query_Event returns a Receive_Operation. The Receive_Query_Event procedure starts or restarts an existing receive operation.
Wait
Use Wait_All, Wait_Some, Wait_At_Least, Wait_For_Success, or Wait_For_Successes. Timers and gates can occupy other slots in the same set.
Finish
After a wait reports a terminal operation, call the Finish overload for that operation type. For a receive operation, Finish writes the event to its output parameter. Finish raises any retained exception.

A connection permits one in-flight PostgreSQL operation. Use a separate connection and session for each database operation that must be active in the same completion set. On one session, start the next operation only after the previous operation becomes terminal.

Each active operation uses one completion-set slot. A receive operation owns one Backend_Message result until Finish.

compose a query after startup completes
Event   : Protocol.Backend_Message;
Receive : Client.Receive_Operation (Set'Access);

declare
   Send : Client.Send_Operation :=
     Client.Send_Query
       (Set'Access,
        Session'Access,
        "select id, value from items",
        Timeout => 5.0);
begin
   Operations.Wait_All (Set);
   Client.Finish (Send);
end;

loop
   Client.Receive_Query_Event
     (Session'Access, Timeout => 5.0, Operation => Receive);
   Operations.Wait_All (Set);
   Client.Finish (Receive, Event);
   Consume (Event);
   exit when Protocol.Response_Kind (Event) =
     Protocol.Ready_For_Query_Response;
end loop;

The example reuses one receive operation to avoid creating a new operation object for each message. Session is aliased and uses an aliased connection transport.

Each operation retains a timeout, cancellation, or provider failure until Finish. The client reports a PostgreSQL query error as an Error_Response event.

For TLS, first start and finish a Negotiate_TLS operation. Then start and finish the Flyology connection TLS upgrade on the same connection. Finally, start and finish a Startup operation. Both client calls return a Startup_Operation.

Use the synchronous APIs when the application does not need composition. They drive the same bounded PostgreSQL protocol state.

STEP 05

Receive simple-query results one event at a time

Stream
Send_Query starts the operation. Each Receive_Query_Event call returns one Simple_Query_Event.
Accessors
Response_Kind classifies the event. Use Description, Row_Data, or Diagnostic_Data for the matching response.
bounded simple-query loop
Client.Send_Query (Session, "select id, value from items");
loop
   declare
      Event : constant Client.Simple_Query_Event :=
        Client.Receive_Query_Event (Session);
   begin
      case Protocol.Response_Kind (Event) is
         when Protocol.Row_Description_Response =>
            Inspect (Protocol.Description (Event));
         when Protocol.Data_Row_Response =>
            Consume (Protocol.Row_Data (Event));
         when Protocol.Error_Response | Protocol.Notice_Response =>
            Report (Protocol.Diagnostic_Data (Event));
         when others =>
            null;
      end case;
      exit when Protocol.Response_Kind (Event) =
        Protocol.Ready_For_Query_Response;
   end;
end loop;

Multiple SQL statements produce multiple event sequences before the final ReadyForQuery. A present zero-length column remains distinct from SQL NULL.

STEP 06

Use prepared statements and bounded portals

The extended path models named or unnamed statements and portals. Prepare_Statement creates statement state. Bind_Portal binds values such as Text_Parameter.

Describe_Portal requests metadata, and Execute_Portal applies a row limit. Flush requests pending output without ending the cycle.

a portal limited to 50 rows
Client.Prepare_Statement
  (Session, "items", "select id, value from items where id > $1", (1 => 23));
Client.Bind_Portal
  (Session,
   Portal_Name    => "items_page",
   Statement_Name => "items",
   Parameters     => (1 => Protocol.Text_Parameter ("100")));
Client.Describe_Portal (Session, "items_page");
Client.Execute_Portal (Session, "items_page", Maximum_Rows => 50);
Client.Flush (Session);

loop
   Event := Client.Receive_Extended_Event (Session);
   exit when Protocol.Response_Kind (Event) in
     Protocol.Command_Complete_Response |
     Protocol.Portal_Suspended_Response |
     Protocol.Error_Response;
end loop;

Client.Synchronize (Session);

Receive_Extended_Event returns one response. The example stops on Command_Complete_Response, Portal_Suspended_Response, or the error response linked above.

Synchronize sends Sync to end or recover the cycle. Continue receiving events through ReadyForQuery before reusing the session.

STEP 07

Pipeline batches without waiting for each result

The extended path normally finishes one Sync-terminated batch before it writes the next batch. Every batch then costs a round trip.

Enter_Pipeline_Mode removes that wait. After Synchronize, the next batch-opening call starts the following batch immediately, so several batches stay in flight. These calls open a batch:

Flush does not open a batch. It only asks the server for the output that the open batch has already produced.

Pending_Synchronizations counts the batches whose ReadyForQuery has not arrived. In_Pipeline_Mode reports the mode. Exit_Pipeline_Mode returns the session to one batch at a time. The session must be idle first, with no batch open and no response outstanding.

one batch per value, all written before the first read
Client.Enter_Pipeline_Mode (Session);
Client.Prepare_Statement
  (Session, "insert_row", "insert into items (value) values ($1)",
   (1 => 25));

for Value of Values loop
   Client.Bind_Portal
     (Session,
      Portal_Name    => "",
      Statement_Name => "insert_row",
      Parameters     => (1 => Protocol.Text_Parameter (Value)));
   Client.Execute_Portal (Session, "");
   Client.Synchronize (Session);
end loop;

--  Every batch is already written before the first response is read.
while Client.Pending_Synchronizations (Session) > 0 loop
   Event := Client.Receive_Extended_Event (Session);
   --  Each Ready_For_Query_Response ends one batch, in send order.
end loop;

Client.Exit_Pipeline_Mode (Session);

The example prepares insert_row inside the first batch, so every later batch depends on it. If that statement fails to parse, each following bind fails as well. When the SQL is not already known to be valid, prepare it in a batch that the loop drains first.

Responses arrive in the order the client wrote the batches, and each ReadyForQuery ends exactly one batch.

The server confines a failure to the batch that caused it, provided that batch has already written its Sync. The server abandons the rest of that batch and sends its ReadyForQuery. It then processes the batch behind it normally, so the session does not enter the recovery state. A batch that fails before its Sync is written still requires Synchronize, exactly as it does outside pipeline mode.

PostgreSQL sends RowDescription only in reply to Describe_Statement or Describe_Portal. A portal that the client executes without one returns bare rows, and Receive_Extended_Event accepts them. It checks the column count only against a description that the server sent, so describe a batch when you want that check.

STEP 08

Stream COPY frames without an accumulator

A COPY response changes the session state to COPY IN, COPY OUT, or COPY BOTH. Each operation sends or receives one bounded chunk. The same typed path works after simple or extended queries.

The example reads the format with Protocol.Copy_Formats. It sends chunks with Send_Copy_Data and completes input with Finish_Copy.

COPY IN
Client.Send_Query
  (Session, "copy measurements from stdin (format text)");

declare
   Started : constant Client.Simple_Query_Event :=
     Client.Receive_Query_Event (Session);
begin
   Inspect (Protocol.Copy_Formats (Started));
   Client.Send_Copy_Data (Session, First_Chunk);
   Client.Send_Copy_Data (Session, Second_Chunk);
   Client.Finish_Copy (Session);
end;

-- Receive CommandComplete, then ReadyForQuery.

Abort_Copy sends CopyFail with a server-visible reason. For extended COPY, synchronization remains mandatory before the session returns to ready.

STEP 09

Stream physical or logical replication in either direction

Flyology.Postgres.Replication handles startup, LSN text, commands, physical stream envelopes, keepalives, status updates, and hot-standby feedback.

Flyology.Postgres.Replication.Logical adds typed pgoutput messages. Logical.Producer validates and encodes them in the primary direction. Both paths deliver one COPY BOTH frame per call.

Physical standby
Start with Physical_Replication_Connection. Send Start_Physical, then consume the WAL bytes in each XLogData frame.
Logical consumer
Start with Logical_Replication_Connection. Send Start_Logical, then decode each XLogData payload.
Protocol levels
Logical protocol v1 covers committed transactions, v2 streamed transactions, v3 two-phase transactions, and v4 parallel-stream abort metadata.
Primary server
Managed_Primary composes application-owned slots, WAL, timelines, and logical changes. Use Replication.Server_Sessions for lower-level control.
Durable state
Replication.Persistence defines interfaces for slots, retained WAL, timeline history, and prepared-consumer recovery. The application supplies the storage technology.

Consume a logical stream without buffering WAL

Replication startup uses Client.Startup with an explicit replication mode. Logical Option values preserve empty and quoted text.

Send the command with Client.Send_Command. Configure the stateful decoder with Logical.Configure and the negotiated Parallel mode.

Receive_Copy_Event returns one frame. A Copy_Data_Response contains replication data.

The example uses the backend-event Original_Message overload, then calls Replication.Decode.

The stream-message Kind identifies XLog_Data. Pass its Data to the stateful Logical.Decode overload.

Do not acknowledge an LSN when its bytes arrive. Advance the durable acknowledgement only after the complete transaction is durably applied.

logical pgoutput v4 consumer
Client.Startup
  (Session,
   User             => "replicator",
   Database         => "app",
   Password         => Password,
   Replication_Mode => Protocol.Logical_Replication_Connection);

Client.Send_Command
  (Session,
   Replication.Start_Logical
     ("subscriber_1",
      Start_LSN,
      (Replication.Option ("proto_version", "4"),
       Replication.Option ("publication_names", "app_publication"),
       Replication.Option ("streaming", "parallel"))));

-- Receive and validate CopyBothResponse before reading COPY events.
Started := Client.Receive_Query_Event (Session);
Logical.Configure (Decoder, Version => 4, Streaming => Logical.Parallel);

loop
   Event := Client.Receive_Copy_Event (Session);
   if Protocol.Response_Kind (Event) = Protocol.Copy_Data_Response then
      Frame := Replication.Decode (Protocol.Original_Message (Event));

      if Replication.Kind (Frame) = Replication.XLog_Data then
         Change := Logical.Decode (Decoder, Replication.Data (Frame));
         Stage (Change);  -- Application-owned transaction state.

         if Logical.Kind (Change) = Logical.Commit_Message then
            Durable_End := Logical.End_LSN (Change);

            -- Atomically commit the target changes and persist Durable_End,
            -- or make replay idempotent if those use separate stores.
            Commit_Target_And_Checkpoint (Durable_End);

            Client.Send_Command
              (Session,
               Replication.Make_Standby_Status_Update
                 (Received_LSN => Durable_End,
                  Flushed_LSN  => Durable_End,
                  Applied_LSN  => Durable_End,
                  Sent_At      => Now));
         end if;
      end if;
   end if;
end loop;

The message Logical.Kind identifies a Commit_Message. Read its durable endpoint with End_LSN, then construct feedback with Make_Standby_Status_Update.

The abbreviated example shows the regular Commit path. Apply the same rule at StreamCommit.

For two-phase traffic, Prepared_Consumer coordinates durable preparation, target application, an applied marker, source acknowledgement, and removal. Unless the target commit and applied marker share one atomic transaction, the target callback must remain idempotent.

Compose a managed primary from application state

The managed-primary generic accepts the persistence interfaces and an application callback for typed logical changes. Its Primary composes those dependencies.

Managed.Initialize sets the system identity. Managed.Handle processes one decoded replication command.

application-owned managed primary
package Managed is new
  Flyology.Postgres.Replication.Managed_Primary
  (Logical_Context => Application_Changes,
   Next_Logical    => Next_Logical);

Primary : Managed.Primary
  (Slots          => Slot_Store'Access,
   WAL            => WAL_Store'Access,
   Timelines      => Timeline_Store'Access,
   Logical_Source => Changes'Access);

Managed.Initialize
  (Primary, System_Id => System_Id, Database => "app");

Command := Replication.Decode_Command (Message);
Managed.Handle (Primary, Client, Command);

The generic Persistence.Memory child is a bounded reference implementation for tests and ephemeral single-owner servers. It is volatile and does not provide process locking or crash recovery.

Control COPY BOTH directly when needed

The lower-level path begins with Decode_Command. The command Kind overload distinguishes values such as Identify_System_Command and Start_Physical_Command.

Reply to identification with Send_Identify_System. For physical replication, Begin_Streaming starts COPY BOTH, and Send_XLog_Data writes one WAL frame.

Read feedback with Read_Standby_Message. The Position accessor returns the requested starting LSN.

primary-side replication handler
Command := Replication.Decode_Command (Message);

case Replication.Kind (Command) is
   when Replication.Identify_System_Command =>
      Replication_Sessions.Send_Identify_System
        (Client, System_Id, Timeline, Current_WAL, Timeout => 10.0);

   when Replication.Start_Physical_Command =>
      Replication_Sessions.Begin_Streaming (Client, Timeout => 10.0);
      Replication_Sessions.Send_XLog_Data
        (Client,
         WAL_Start => Replication.Position (Command),
         WAL_End   => Current_WAL,
         Sent_At   => Now,
         Data      => WAL_Chunk,
         Timeout   => 10.0);

      Feedback := Replication_Sessions.Read_Standby_Message
        (Client, Timeout => 10.0);
      Apply_Feedback (Feedback);

   when others =>
      -- Handle another supported replication command.
      null;
end case;

For graceful shutdown, call Finish_Streaming to close the primary-to-standby direction. Continue reading feedback until the frontend CopyDone arrives, then call Complete_Streaming. Send an error response after a frontend CopyFail.

Promotion belongs to the application-owned timeline and WAL stores. First, persist the new timeline and its history. Retain WAL across the fork point before advertising the timeline as current.

The follower can then request TIMELINE_HISTORY and reconnect on the promoted timeline. The PostgreSQL 18 test promotes a real standby and verifies replay of WAL written only on timeline 2.

Search the generated replication API reference

STEP 10

Take a physical base backup without pg_basebackup

Flyology.Postgres.Replication.Base_Backups exposes the replication-protocol operation that pg_basebackup uses. The application still owns transport setup, storage, durability, and extraction policy.

The package models the distinct PostgreSQL 14 and 15+ wire streams. It adds PostgreSQL 17+ manifest upload and incremental options. Each receive operation returns one owned event.

Open a physical replication session

BASE_BACKUP requires a physical replication connection. Authenticate through the normal client startup path. Set Replication_Mode to Physical_Replication_Connection.

Construct Options with the actual server major. The major selects a capability boundary, not a preferred syntax. PostgreSQL 14 options cannot enable compression or incremental mode. PostgreSQL 15 options cannot upload a prior manifest.

physical-replication startup and backup policy
Client.Startup_TLS
  (Session,
   Backend,
   Server_Name      => "db.example.com",
   User             => "backup_agent",
   Database         => "postgres",
   Password         => Password,
   Replication_Mode => Protocol.Physical_Replication_Connection,
   Timeout          => 10.0);

Settings := Base_Backups.Defaults (Server_Major);
Base_Backups.Set_Label (Settings, "nightly-2026-08-13");
Base_Backups.Set_Checkpoint
  (Settings, Base_Backups.Spread_Checkpoint);
Base_Backups.Include_WAL (Settings);
Base_Backups.Set_Progress (Settings);
Base_Backups.Include_Tablespace_Map (Settings);
Base_Backups.Set_Manifest
  (Settings,
   Base_Backups.Include_Manifest,
   Base_Backups.SHA256_Checksum);

Include_WAL asks PostgreSQL to place the WAL required for recovery in the backup. If this option is disabled, the application must retain the required WAL independently.

Wait_For_Archive controls whether PostgreSQL waits for the ending segment to reach its WAL archive. This option does not make local storage durable. Set_Maximum_Rate accepts zero or 32 through 1,048,576 KiB/s.

PostgreSQL 14
Legacy command keywords and a separate COPY OUT stream for every tablespace archive plus an optional manifest. Archive identity follows tablespace order; there is no server-sent archive-name frame.
PostgreSQL 15–16
Parenthesized options, client/server/blackhole targets, gzip/LZ4/zstd compression, and one COPY OUT stream multiplexed with archive, manifest, data, and progress markers.
PostgreSQL 17–18
The PostgreSQL 15+ stream plus UPLOAD_MANIFEST followed by an INCREMENTAL backup. The server must have summarize_wal=on.

Stage each archive and manifest

Create a private staging destination before the backup starts. Tablespace rows arrive before archive bytes and preserve SQL NULL. The base directory has no OID or external location. A size can be absent when progress was not requested.

Use Stream_Index as the stable event key. On PostgreSQL 15+, Archive_Name and Archive_Location expose the multiplexed marker. On PostgreSQL 14, map each stream ordinal to its preceding tablespace row.

bounded storage and consistency loop
Backup : Base_Backups.Receiver (Session'Access);

Base_Backups.Start (Backup, Settings);

loop
   declare
      Event : constant Base_Backups.Event := Base_Backups.Receive (Backup);
   begin
      case Base_Backups.Kind (Event) is
         when Base_Backups.Backup_Start =>
            Start_LSN := Base_Backups.Start_LSN (Event);
            Timeline := Base_Backups.Timeline (Event);

         when Base_Backups.Tablespace =>
            Register_Tablespace
              (Has_Oid      => Base_Backups.Has_Tablespace_Oid (Event),
               Has_Location =>
                 Base_Backups.Has_Tablespace_Location (Event),
               Has_Size     => Base_Backups.Has_Tablespace_Size (Event));

         when Base_Backups.Archive_Start =>
            Begin_Archive
              (Index => Base_Backups.Stream_Index (Event),
               Name  =>
                 (if Server_Major >= 15
                  then Base_Backups.Archive_Name (Event)
                  else Legacy_Archive_Name
                    (Base_Backups.Stream_Index (Event))));

         when Base_Backups.Archive_Data =>
            Write_Archive_Chunk
              (Base_Backups.Stream_Index (Event),
               Base_Backups.Data (Event));

         when Base_Backups.Manifest_Start =>
            Begin_Manifest;
         when Base_Backups.Manifest_Data =>
            Write_Manifest_Chunk (Base_Backups.Data (Event));
         when Base_Backups.Progress =>
            Report_Archive_Progress
              (Base_Backups.Bytes_Completed (Event));

         when Base_Backups.Backup_End =>
            End_LSN := Base_Backups.End_LSN (Event);
            Saw_Consistent_End :=
              Base_Backups.Timeline (Event) = Timeline
              and then End_LSN >= Start_LSN;

         when Base_Backups.Notice =>
            Log_Notice (Base_Backups.Diagnostic (Event));
         when Base_Backups.Parameter_Status =>
            Apply_Status (Base_Backups.Status (Event));
         when Base_Backups.Error =>
            Failed := True;
            Log_Error (Base_Backups.Diagnostic (Event));
            -- Keep receiving so ReadyForQuery can restore the session.
         when Base_Backups.Complete =>
            exit;
      end case;
   end;
end loop;

Each Receive call returns at most one PostgreSQL CopyData payload. The 16 MiB protocol limit bounds each payload. Write the current chunk, or give it to another bounded queue, before the next call.

A progress event reports bytes completed in the current archive or tablespace. It does not report a whole-backup total. The receiver validates marker order, archive counts, tablespace counts, manifest presence, signed progress values, timelines, and LSN order.

PostgreSQL streams ustar data without the two terminal zero blocks. Append the blocks before using a strict reader, or use a reader that accepts this convention.

Close and sync each sink after Backup_End. Publish the staging destination only after Complete confirms that the server is ready. On another outcome, quarantine the destination or remove it through the application's recoverable cleanup policy.

Upload the prior manifest before an incremental backup

PostgreSQL 17 and 18 calculate an incremental backup from a complete prior backup manifest. Retain that manifest as a separate durable object.

Call Begin_Manifest_Upload, then send bounded chunks with Send_Manifest_Chunk. Call Finish_Manifest_Upload, then drain COPY and query completion. PostgreSQL validates the manifest chain and required WAL summaries.

PostgreSQL 17+ incremental preparation
Base_Backups.Begin_Manifest_Upload
  (Session, Server_Major, Timeout => 10.0);

while Prior_Manifest.Has_More loop
   Base_Backups.Send_Manifest_Chunk
     (Session, Prior_Manifest.Next_Chunk (32 * 1_024), Timeout => 10.0);
end loop;
Base_Backups.Finish_Manifest_Upload (Session, Timeout => 10.0);

while not Client.Is_Ready (Session) loop
   if Client.State (Session) in
     Client.Copy_In_Active | Client.Copy_Completion_Active
   then
      Check (Client.Receive_Copy_Event (Session, Timeout => 10.0));
   else
      Check (Client.Receive_Query_Event (Session, Timeout => 10.0));
   end if;
end loop;

Incremental := Base_Backups.Defaults (Server_Major);
Base_Backups.Set_Incremental (Incremental);
Base_Backups.Set_Manifest
  (Incremental, Base_Backups.Include_Manifest);
Base_Backups.Start (Backup, Incremental, Timeout => 10.0);

After the upload completes, enable Set_Incremental on a new option set. Store the new manifest with its backup for the next link in the chain.

An incremental stream is not a standalone data directory. PostgreSQL recovery tooling must combine it with its ancestor backups.

Cancel separately, then drain the active session

Cancellation does not travel on the connection that carries archive data. Open another transport to the same server and apply the same TLS policy. Pass that transport to Base_Backups.Cancel.

PostgreSQL closes the cancellation transport without a response. Continue the main receive loop until Complete. A cancellation diagnostic normally has SQLSTATE 57014.

Use the same sequence after a local disk error. Stop publishing data, request cancellation from another task, and drain the protocol before session reuse.

Keep server semantics above the framing layer

Replication.Decode_Command classifies BASE_BACKUP and PostgreSQL 17+ UPLOAD_MANIFEST. It preserves the original owned query for routing.

Base_Backups.Server_Sessions sends positions, nullable tablespace rows, COPY streams, multiplexed markers, progress, and manifest COPY IN. The package does not walk a live data directory or claim universal server support.

Application policy
Authorize a dedicated replication identity, select allowed targets and paths, enforce rate and compression policy, and reject capabilities the backend cannot honor.
Consistency
Enter and leave PostgreSQL-compatible backup mode, retain the WAL interval, and produce start/end LSNs and timelines from one coherent backup operation.
Storage
Create tar archives, tablespace mappings, backup labels, manifests, checksums, durable staging, and failure cleanup outside the protocol package.
Proxying
Forward cancellation and backpressure as well as bytes. Do not acknowledge completion to the downstream client before upstream completion and durable downstream writes.

Replace a disposable pg_basebackup sidecar

For an example such as psqlbench, keep the primary container and its readiness checks. Remove the container or subprocess that only invokes pg_basebackup.

Run the client-target flow into an example-owned staging directory. Include WAL and map external tablespaces only beneath disposable roots. Publish the directory after consistent end and completion events. Then start the disposable PostgreSQL instance from that directory.

This checkout has no examples/psqlbench tree. The protocol slice therefore documents the migration without adding a machine-specific substitute.

Read the full compatibility, incremental-backup, cancellation, security, and migration guide

STEP 11

Cancel on a separate connection

Postgres cancellation does not travel on the active query connection. Startup stores the backend process ID and variable-length secret.

Flyology.Postgres.Client_Sockets supplies socket-based cancellation. Cancel_TLS verifies a separate encrypted connection, sends the credentials, and closes without waiting for a response.

cancel an active operation
Client_Sockets.Cancel_TLS
  (Session, Server, Backend, "db.example.com", Timeout => 5.0);

-- Keep receiving on Session until the server's ErrorResponse and
-- ReadyForQuery complete normal recovery.

Issue cancellation from an independently scheduled task when a lightweight receiver may be continuously runnable.

STEP 12

Build a Postgres protocol server

Flyology.Postgres.Server is generic over application context, authentication callbacks, SCRAM verifier lookup, and a command handler. Each accepted connection receives one Flyology handler task.

Commands
The handler receives every normal frontend command as a typed or raw-preserving Protocol.Message.
Responses
Session helpers send row descriptions, rows, diagnostics, completion, readiness, and COPY streams.
Authentication
SCRAM lookup returns Postgres verifier text. The server does not request or retain the user's plaintext password.
Cancellation
A fresh per-command token reports matching cancellation and forced structured-server shutdown.
TLS
Serve_TLS upgrades accepted connections without changing their admission owner. Required mode rejects plaintext startup.

Search the generated server and session API

STEP 13

Parse, analyze, and transform SQL

The versioned SQL crates parse PostgreSQL 14 through 18 syntax with native Ada parsers generated from each pinned upstream grammar. Select the crate for the PostgreSQL major that the application accepts.

Use one of these owned packages for ordinary application code:

add only the PostgreSQL 18 parser
alr with flyology_postgres_sql_v18

Import sql_v18.gpr from the application project. To support several majors, depend on and import each desired flyology_postgres_sql_vNN/sql_vNN.gpr pair; they link the shared parser core once and keep distinct versioned AST types. Use flyology_postgres_sql and sql.gpr only as the compatibility umbrella when all five majors must be available at runtime.

combine selected PostgreSQL majors
alr with flyology_postgres_sql_v14
alr with flyology_postgres_sql_v18

# In the application GPR project:
with "sql_v14.gpr";
with "sql_v18.gpr";

The example stores the result in Owned_Syntax_Tree and calls the V18 Parse operation. The tree's Valid, Diagnostic_Message, and Diagnostic_Position fields report failure.

parse and inspect a PostgreSQL 18 SELECT
with Flyology.Postgres.SQL.AST.V18;

declare
   package AST renames Flyology.Postgres.SQL.AST.V18;
   Tree : AST.Owned_Syntax_Tree;
begin
   AST.Parse
     ("SELECT id, payload FROM events WHERE archived = false",
      Tree);

   if not Tree.Valid then
      Report_Parse_Error (Tree.Diagnostic_Message, Tree.Diagnostic_Position);
   end if;
end;

Walk the complete tree with a generated visitor

Each owned AST has a generated visitor child package. For V18, use SQL.AST.V18.Visitors.

Derive an analysis type from Visitor. Override typed hooks such as Enter_Select_Stmt. This hook receives a Select_Stmt and a Traversal_Control value.

The example counts expressions through the statement's Target_List field.

The owned-tree Traverse overload follows each present child and each message element in a typed vector.

count SELECT statements and target expressions
with Flyology.Postgres.SQL.AST.V18;
with Flyology.Postgres.SQL.AST.V18.Visitors;

declare
   package AST renames Flyology.Postgres.SQL.AST.V18;
   package Visitors renames Flyology.Postgres.SQL.AST.V18.Visitors;

   type Analysis is new Visitors.Visitor with record
      Selects : Natural := 0;
      Targets : Natural := 0;
   end record;

   overriding procedure Enter_Select_Stmt
     (Self    : in out Analysis;
      Item    : AST.Select_Stmt;
      Control : in out Visitors.Traversal_Control)
   is
      pragma Unreferenced (Control);
   begin
      Self.Selects := Self.Selects + 1;
      Self.Targets := Self.Targets + Natural (Item.Target_List.Length);
   end Enter_Select_Stmt;

   Tree   : AST.Owned_Syntax_Tree;
   Report : Analysis;
begin
   AST.Parse
     ("WITH q AS (SELECT id FROM events) SELECT id FROM q",
      Tree);

   if Tree.Valid then
      Visitors.Traverse (Report, Tree);
   end if;
end;

An enter hook may set Skip_Children when descendants cannot add facts. It may set Stop_Traversal when the analysis is complete. Matching leave hooks support scopes, stacks, and post-order summaries.

Plan transformations during traversal, then apply them

Visitor callback records are borrowed, read-only views of the owned graph. For a safe rewrite, collect child access values in visitor state, finish the traversal, then mutate scalar payloads through those references. This keeps vectors and graph topology stable while the walker is active.

The rewrite example overrides Enter_A_Const and receives an A_Const record.

Its Sval field is an optional string access value. Check its Present field before reading its Value.

The referenced string record has its own Sval field. Its text Present and Value fields control the replacement text.

collect and redact owned string literals
overriding procedure Enter_A_Const
  (Self    : in out Rewrite_Planner;
   Item    : AST.A_Const;
   Control : in out Visitors.Traversal_Control)
is
   pragma Unreferenced (Control);
begin
   if Item.Sval.Present and then Item.Sval.Value /= null then
      Self.String_Literals.Append (Item.Sval.Value);
   end if;
end Enter_A_Const;

Visitors.Traverse (Plan, Tree);

for Literal of Plan.String_Literals loop
   if Literal.Sval.Present then
      Literal.Sval.Value := To_Unbounded_String ("[redacted]");
   end if;
end loop;

Run the analysis and transformation examples

The repository includes strict Ada 2022 examples that count relations, joins, columns, and function calls, then demonstrate a two-pass relation rename and literal redaction.

build and run the SQL visitor examples
cd sql/examples
alr -n build
./scripts/test.sh

./bin/analyze_sql "SELECT count(*) FROM audit.events"

Read the complete runnable visitor examples

Owned AST
SQL.AST.V14 through V18 expose records, typed vectors, enums, access values, and explicit Present discriminants. This is the default API.
Visitors
Every reachable message has generated typed Enter_* and Leave_* hooks, plus pruning and early-stop controls.
Transformations
Build a rewrite plan during traversal and apply it afterward. The in-memory AST changes, but Source_Text remains the original SQL.
Lifetime
Owned_Syntax_Tree controls the complete graph. Do not retain its access values after clearing, reparsing, or finalizing that tree.
Shallow views
SQL.Views.V14, V15, V16, V17, and V18 avoid the recursive object graph. Use them only when allocation pressure justifies resolving opaque references against their owning syntax tree.
Diagnostics
A failed owned parse leaves Tree.Valid false and records the message and cursor position in the owning tree.
SQL output
The crate currently has no deparser. Emit a separate target representation or pair the rewrite plan with a dedicated SQL printer when rewritten text is required.

Read the complete parser API and architecture guide

Search the generated SQL API reference

STEP 14

Run the example crates

The examples are independent nested Alire crates. pgish can require TLS from a configured certificate and key; psqlish supports verified TLS with separate certificate and connection host settings. Their integration uses an ephemeral CA in both directions.

example directories
cd examples/psqlish
alr run

cd ../pgish
alr run
STEP 15

Check the current boundaries before evaluation

The library is experimental. Treat these constraints as part of its public contract, not as incidental implementation details.

TLS
Verified client and ownership-preserving server upgrades use PostgreSQL SSLRequest; required mode rejects plaintext startup. PostgreSQL's separate sslnegotiation=direct mode is not implemented.
Authentication
Trust, cleartext password, and SCRAM-SHA-256 only. No MD5, GSSAPI, SSPI, certificate authentication, or SCRAM-SHA-256-PLUS.
Cleartext
When the transport is not verified TLS or comparably trusted, do not use cleartext-password authentication.
Fairness
A continuously readable COPY socket may require an explicit Flyology fairness point so another task on the same group can run.
Replication
Protocol support covers PostgreSQL physical streaming and pgoutput v1 through v4. Flyology supplies managed slot lifecycle and storage interfaces, but applications still supply the durability technology, retained WAL and logical-change sources, publication catalog, stable initial snapshot, and target application of decoded changes.
Limits
SCRAM messages are bounded to 4 KiB; iteration counts range from 4,096 through 1,000,000.