PostgreSQL
15 min read • 3,136 wordsAvailable v4.12.0 Released October 31, 2026
Overview
PostgreSQL is supported as both a source (extract from tables and views) and a target (load and historize into a PostgreSQL warehouse or database). It works with self-hosted PostgreSQL and managed services that expose a standard PostgreSQL endpoint. PostgreSQL connects to Amfelt’s data plane through Apache Arrow: the connector hands records to the agent as Arrow record batches.
Support for pgvector vector properties is on the roadmap.
Capabilities
| Feature | Source | Target |
|---|---|---|
| Full load | ||
| Incremental load | — | |
| Change Data Capture (CDC) | — | — |
| Schema drift | — |
Prerequisites
Before connecting PostgreSQL:
- Network access from the Amfelt agent to the PostgreSQL host and port (default
5432). - A database and schema to read from (source) or write to (target).
- A login role with the privileges in the Required privileges section below.
- Supported versions: PostgreSQL 12 and later as a source; PostgreSQL 15 and later as a
target. The target’s history tables use a unique index with
NULLS NOT DISTINCT, which PostgreSQL added in version 15, so the target’s connection test refuses an older server.
Required Privileges
Create a dedicated, least-privilege login role and grant it only what it needs: the source grants for extraction, the target grants for loading and historization. Store the password in your key vault; it is never part of the connection descriptor.
Source (Read-Only)
-- PostgreSQL source: least-privilege, read-only.
-- Replace orders / public / amfelt_src to match your environment; store the
-- password in the Amfelt key vault (never inline in a deployed grant).
CREATE ROLE amfelt_src LOGIN PASSWORD '***';
GRANT CONNECT ON DATABASE orders TO amfelt_src;
GRANT USAGE ON SCHEMA public TO amfelt_src;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO amfelt_src;
-- Future tables created in the schema are readable automatically:
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO amfelt_src;Target (Read-Write + DDL)
-- PostgreSQL target: read-write plus the DDL rights to create the raw/staging/
-- integration/bridge layer objects Amfelt generates. Store the password in the
-- Amfelt key vault.
CREATE ROLE amfelt_tgt LOGIN PASSWORD '***';
GRANT CONNECT ON DATABASE warehouse TO amfelt_tgt;
-- Create and own the warehouse layer schemas, and the state schemas <prefix>_sys and
-- <prefix>_qrn, on which Amfelt revokes every PUBLIC right:
GRANT CREATE ON DATABASE warehouse TO amfelt_tgt;
-- Temporary tables: delta and window loads compute their compare once per apply in one.
-- PUBLIC holds it by default; the grant keeps it where PUBLIC has lost it:
GRANT TEMPORARY ON DATABASE warehouse TO amfelt_tgt;
GRANT USAGE, CREATE ON SCHEMA public TO amfelt_tgt;
-- Read-write on the objects Amfelt manages:
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO amfelt_tgt;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO amfelt_tgt;
-- Give a consumer role USAGE on the bridge schema <prefix>_bdg only, never on <prefix>_sys
-- or <prefix>_qrn. A load's bridge apply recreates the bridge views without their grants,
-- so the default privilege grants SELECT on each view amfelt_tgt creates there. After the
-- first deploy, run as amfelt_tgt, with the prefix in place of sales and the consumer role
-- in place of reporting:
-- GRANT USAGE ON SCHEMA sales_bdg TO reporting;
-- GRANT SELECT ON ALL TABLES IN SCHEMA sales_bdg TO reporting;
-- ALTER DEFAULT PRIVILEGES FOR ROLE amfelt_tgt IN SCHEMA sales_bdg GRANT SELECT ON TABLES TO reporting;Connect
- In Amfelt, add a PostgreSQL source or target and select the PostgreSQL connector.
- Provide the connection properties (see Configuration reference):
host,port,database, andschema. - Store the login credentials in the configured key vault — they are never part of the connection descriptor.
- Validate the connection and choose Discover schema.
A connection resolves to an ODCS server descriptor like:
servers:
- server: postgres-prod
type: postgres
host: db.example.com
port: 5432
database: orders
schema: publicIncremental Extracts
The source performs full extracts and incremental extracts. An incremental extract reads
only the rows whose cursor column is at or after the lower bound of the run. The cursor is
a timestamp (a timestamp or timestamptz column) or a sequence (a smallint, integer
or bigint column); a cursor on a column of any other type fails the run instead of
reading the object in full. A timestamp cursor (without time zone) is compared as a UTC
wall clock, whatever the session’s TimeZone defaults to. An index whose leading column is
the cursor column lets PostgreSQL read a small increment without scanning the table.
To keep a transaction that is still open from hiding rows below the next bound, the source
caps its clock at the start of the oldest open transaction of other sessions. PostgreSQL
shows those sessions only to a superuser or a member of pg_read_all_stats; grant that
role to the source user to get the cap. Without it the source still extracts correctly, but
the clock is not capped.
Authentication
Credentials are supplied through the Amfelt key vault, not the ODCS connection
topology: a database username and password, and an optional sslmode
(disable, require, verify-full). The connection descriptor above intentionally
contains no credential fields.
The password must not contain whitespace, a single quote ('), or a backslash (\): the
connection string cannot carry those characters yet, so the connector refuses such a
password before it connects.
State Schemas
A target deploy that creates the history layer also creates the state schema <prefix>_sys.
It holds what each history apply commits with its rows: the watermark of each object and the
ledger of the files loaded. The first quarantined file of an object creates <prefix>_qrn,
when it does not exist yet, and the object’s quarantine table in it, which holds the
quarantined rows until they are purged. The target role creates both
schemas, so it owns them, and Amfelt revokes every right PUBLIC holds on them. No bridge
view reads either schema.
Give a consumer role USAGE on the bridge schema <prefix>_bdg only, never on
<prefix>_sys or <prefix>_qrn. A load’s bridge apply drops and recreates the bridge views,
and a recreated view has none of the old view’s grants, so grant SELECT on the views there
now and, as a default privilege of the target role, on each view it creates there later. Run
these as the target role after the first deploy, with the prefix in place of sales, the
consumer role in place of reporting and the target role in place of amfelt_tgt:
GRANT USAGE ON SCHEMA sales_bdg TO reporting;
GRANT SELECT ON ALL TABLES IN SCHEMA sales_bdg TO reporting;
ALTER DEFAULT PRIVILEGES FOR ROLE amfelt_tgt IN SCHEMA sales_bdg GRANT SELECT ON TABLES TO reporting;Sync Modes
| Mode | What it does |
|---|---|
| Full load | Reloads all selected data on every run. |
| Incremental load | Loads only records changed since the last run, tracked by a watermark column (timestamp or sequence). |
Target History Modes
| Mode | What it does |
|---|---|
| SCD Type 2 (full history) | Keeps the full history. Each change writes a new versioned record with validity dates. |
| SCD Type 1 (overwrite) | Overwrites in place, keeping only the latest value, no history. |
| Append | Adds new records without updating or removing existing ones. |
| Deduplicated append | Appends new records, skipping duplicates already loaded. |
| Snapshot | Stores a full point-in-time copy of the source on each run. |
| Replace (truncate & load) | Replaces the entire target with the latest load on each run. |
Configuration Reference
| Property | Type | Required | Description |
|---|---|---|---|
database | string | Yes | The name of the database. |
host | string | Yes | The host to the Postgres server |
port | integer | Yes | The port to the Postgres server. |
schema | string | Yes | The name of the schema in the database. |
Type Mapping
Each PostgreSQL physical type maps to an Apache Arrow type (the data-plane intermediate) and then to an ODCS logical type. See the official PostgreSQL data type reference .
| PostgreSQL type | Aliases | Apache Arrow | ODCS logical | Description |
|---|---|---|---|---|
_bool | — | utf8 | string | array of boolean |
_date | — | utf8 | string | array of date |
_float4 | — | utf8 | string | array of real |
_float8 | — | utf8 | string | array of double precision |
_int2 | — | utf8 | string | array of smallint |
_int4 | — | utf8 | string | array of integer |
_int8 | — | utf8 | string | array of bigint |
_numeric | — | utf8 | string | array of numeric |
_text | — | utf8 | string | array of text |
_timestamp | — | utf8 | string | array of timestamp |
_timestamptz | — | utf8 | string | array of timestamptz |
_uuid | — | utf8 | string | array of uuid |
_varchar | — | utf8 | string | array of varchar |
array | — | utf8 | string | PostgreSQL array (any element type) |
bigint | int8 | int64 | integer | signed eight-byte integer |
bigserial | serial8 | int64 | integer | autoincrementing eight-byte integer |
bit | — | utf8 | string | fixed-length bit string |
boolean | bool | bool | boolean | logical Boolean (true/false) |
box | — | utf8 | object | rectangular box on a plane |
bpchar | — | utf8 | string | variable-length character string, blank-padded |
bytea | — | binary | object | binary data (“byte array”) |
character | char | utf8 | string | fixed-length character string |
cidr | — | utf8 | object | IPv4 or IPv6 network address |
circle | — | utf8 | object | circle on a plane |
date | — | date32 | date | calendar date (year, month, day) |
daterange | — | utf8 | string | range of date |
float8 | double precision | float64 | number | double precision floating-point number (8 bytes) |
inet | — | utf8 | string | IPv4 or IPv6 host address |
int4range | — | utf8 | string | range of integer (int4) |
int8range | — | utf8 | string | range of bigint (int8) |
integer | int, int4 | int32 | integer | signed four-byte integer |
interval | — | month_day_nano_interval | object | time span |
json | — | utf8 | string | textual JSON data |
jsonb | — | utf8 | object | binary JSON data, decomposed |
line | — | utf8 | object | infinite line on a plane |
lseg | — | utf8 | object | line segment on a plane |
macaddr | — | utf8 | string | MAC (Media Access Control) address |
macaddr8 | — | utf8 | string | MAC (Media Access Control) address (EUI-64 format) |
money | — | decimal128(19,2) | number | currency amount |
numeric | decimal | — | number | exact numeric of selectable precision |
numrange | — | utf8 | string | range of numeric |
oid | — | int64 | integer | object identifier |
path | — | utf8 | object | geometric path on a plane |
pg_lsn | — | utf8 | object | PostgreSQL Log Sequence Number |
pg_snapshot | — | binary | object | user-level transaction ID snapshot |
point | — | utf8 | object | geometric point on a plane |
polygon | — | utf8 | object | closed geometric path on a plane |
real | float4 | float32 | number | single precision floating-point number (4 bytes) |
serial | serial4 | int32 | integer | autoincrementing four-byte integer |
smallint | int2 | int16 | integer | signed two-byte integer |
smallserial | serial2 | int16 | integer | autoincrementing two-byte integer |
text | — | utf8 | string | variable-length character string |
time with time zone | timetz | timestamp_us_utc | date | time of day, including time zone |
time without time zone | time | timestamp_us_utc | date | time of day (no time zone) |
timestamp with time zone | timestamptz | timestamp_us_utc | date | date and time, including time zone |
timestamp without time zone | timestamp | timestamp_us_utc | date | date and time (no time zone) |
tsquery | — | utf8 | object | text search query |
tsrange | — | utf8 | string | range of timestamp without time zone |
tstzrange | — | utf8 | string | range of timestamp with time zone |
tsvector | — | utf8 | object | text search document |
txid_snapshot | — | binary | object | user-level transaction ID snapshot (deprecated; see pg_snapshot) |
uuid | — | utf8 | string | universally unique identifier |
varbit | bit varying | utf8 | string | variable-length bit string |
varchar | character varying | utf8 | string | variable-length character string |
xml | — | utf8 | string | XML data |
Troubleshooting
listen_addresses and pg_hba.conf permit the agent’s network.CONNECT on the database.'), or a backslash (\) anywhere in it, before it connects: the connection string cannot carry those characters yet. Set a password without them on the role, and update the vaulted credentials to match.syntax error at or near "NULLS" when it creates the history table’s unique index. Upgrade the target server to PostgreSQL 15 or later. Reading from PostgreSQL 12 or later as a source is unaffected.TEMPORARY on the database. That is the login role, unless a role setting switches it to another, such as the connection’s role property or ALTER ROLE … SET role. The connection test shows the grant to run, GRANT TEMPORARY ON DATABASE <database> TO <role>;, but masks the login user as ***, so the role, the database name or another word may read *** in whole or in part. Run the grant on the target’s database, for the role the connection acts as. PostgreSQL gives it to every role by default, so this appears only where it was revoked.01234 arrives this way; earlier versions read it as the number 1234. The target refuses a value that type would change instead of loading it altered: a leading zero, surrounding whitespace, a digit beyond the column’s precision or scale, a decimal that a real or double precision column would round, a boolean spelled other than true or false, a date not written YYYY-MM-DD, or a UTC offset that a timestamp column without a time zone would drop, or that a column with one needs and the value lacks. Text bound for a column of any other type, such as money, jsonb or varchar(n), is not checked, and loads as PostgreSQL reads it. On a refusal the target stops the load with an error that names the column and the row but not the value. Nothing from that load reaches the table, which keeps the rows of the last successful run, and the run fails. From agent version 4.18.6 on, the run reports the target’s error for a load of any size. An earlier agent, such as 4.18.3, can report only WriteRecords send: EOF instead when the source was still sending rows. A deployed column keeps its type when you deploy again under the same schema prefix. Re-ingest the source’s metadata and deploy it under a new schema prefix before you load that source again.SELECT (source) or write/DDL (target). Re-apply the grants in the Required privileges section, including ALTER DEFAULT PRIVILEGES for objects created later.sslmode to require or verify-full in the vaulted credentials.<prefix>_qrn, when it does not exist yet, and the object’s quarantine table in it, so the target role needs CREATE on the database, as it does for a deploy. Without it the quarantine is refused with quarantine_create_denied and the file keeps the ledger status it had. Grant CREATE ON DATABASE <database> to the target role and quarantine the file again.Changelog
Release history for the PostgreSQL connector, newest first.
Features
- Keep the state schema and the file ledger
Features
- Cursor binder for postgres and duckdb sources
Features
- Publish plugin releases on the connectors repo
Bug Fixes
- Move connector module paths to github.com/amfeltlabs
Bug Fixes
- Count scd1 overwrites as updates
- Scaffold rdbms connectors on pgx; require conformance v0.5.0
Features
- Open PostgreSQL with pgx; core v3.41.2, plugin-sdk v5.17.4
Features
- Write the amf_ metadata columns and name Amfelt in what connectors ship
Bug Fixes
- Bump golang.org/x/net to v0.60.0 (GO-2026-6617)
Features
- Ship third-party notices in postgres and file archives
Bug Fixes
- Refuse a text value its column’s type would alter
Features
- Record third-party marks per connector
Features
- Compare scope in the postgres and duckdb templates
Bug Fixes
- Refuse targets older than postgresql 15
Bug Fixes
- ship plugin-sdk v5.12.2 to keep credentials out of exported contracts
Features
- Postgres and duckdb sources answer the source connection test
Bug Fixes
- Require plugin-sdk v5.9.3 so a read-only-default role is not reported ok
Features
- Answer the target connection test
Features
- Pass the declared override surface to the renderer (ADR-117)
Features
- Declare the override surface and govern the declared tier (ADR-117)
Features
- Add the source connector
Breaking Changes
- Adopt plugin-sdk /v5 — re-pin, ctx seams, quoting, BaseRenderer, DTOs
Bug Fixes
- Truthful manifests + capability-flag CI gate
Bug Fixes
- Bump plugin-sdk to v4.19.1
Features
- Serve read-only RunScalarQuery RPC
Features
- Serve CountStagingRows RPC
Features
- Per-property target DDL typer (durable B4b, CSV renders distinct columns)
Features
- Carry source attribution in DDL
Features
- Let the target export its own schema
Features
- Write rows to the database (OB-14)
Features
- Oss dependency licenses for postgres
Features
- Changelog in doc bundle
Features
- Enriched doc bundle
Features
- Connector doc bundle + Hub publish
Features
- Migrate connector to v4 capability sdk
Bug Fixes
- Serve via sdk connector cli
Licensing
Third-party open-source packages bundled by this connector, with their licenses. Attribution only: informational, not an evaluation of the connector.