PostgreSQL

15 min read • 3,136 words
Connect PostgreSQL with Amfelt: setup, required privileges, configuration reference, and type mapping.

Available 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  

FeatureSourceTarget
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  

  1. In Amfelt, add a PostgreSQL source or target and select the PostgreSQL connector.
  2. Provide the connection properties (see Configuration reference): host, port, database, and schema.
  3. Store the login credentials in the configured key vault — they are never part of the connection descriptor.
  4. 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: public

Incremental 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  

ModeWhat it does
Full loadReloads all selected data on every run.
Incremental loadLoads only records changed since the last run, tracked by a watermark column (timestamp or sequence).

Target History Modes  

ModeWhat 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.
AppendAdds new records without updating or removing existing ones.
Deduplicated appendAppends new records, skipping duplicates already loaded.
SnapshotStores 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  

PropertyTypeRequiredDescription
databasestringYesThe name of the database.
hoststringYesThe host to the Postgres server
portintegerYesThe port to the Postgres server.
schemastringYesThe 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 typeAliasesApache ArrowODCS logicalDescription
_bool—utf8stringarray of boolean
_date—utf8stringarray of date
_float4—utf8stringarray of real
_float8—utf8stringarray of double precision
_int2—utf8stringarray of smallint
_int4—utf8stringarray of integer
_int8—utf8stringarray of bigint
_numeric—utf8stringarray of numeric
_text—utf8stringarray of text
_timestamp—utf8stringarray of timestamp
_timestamptz—utf8stringarray of timestamptz
_uuid—utf8stringarray of uuid
_varchar—utf8stringarray of varchar
array—utf8stringPostgreSQL array (any element type)
bigintint8int64integersigned eight-byte integer
bigserialserial8int64integerautoincrementing eight-byte integer
bit—utf8stringfixed-length bit string
booleanboolboolbooleanlogical Boolean (true/false)
box—utf8objectrectangular box on a plane
bpchar—utf8stringvariable-length character string, blank-padded
bytea—binaryobjectbinary data (“byte array”)
charactercharutf8stringfixed-length character string
cidr—utf8objectIPv4 or IPv6 network address
circle—utf8objectcircle on a plane
date—date32datecalendar date (year, month, day)
daterange—utf8stringrange of date
float8double precisionfloat64numberdouble precision floating-point number (8 bytes)
inet—utf8stringIPv4 or IPv6 host address
int4range—utf8stringrange of integer (int4)
int8range—utf8stringrange of bigint (int8)
integerint, int4int32integersigned four-byte integer
interval—month_day_nano_intervalobjecttime span
json—utf8stringtextual JSON data
jsonb—utf8objectbinary JSON data, decomposed
line—utf8objectinfinite line on a plane
lseg—utf8objectline segment on a plane
macaddr—utf8stringMAC (Media Access Control) address
macaddr8—utf8stringMAC (Media Access Control) address (EUI-64 format)
money—decimal128(19,2)numbercurrency amount
numericdecimal—numberexact numeric of selectable precision
numrange—utf8stringrange of numeric
oid—int64integerobject identifier
path—utf8objectgeometric path on a plane
pg_lsn—utf8objectPostgreSQL Log Sequence Number
pg_snapshot—binaryobjectuser-level transaction ID snapshot
point—utf8objectgeometric point on a plane
polygon—utf8objectclosed geometric path on a plane
realfloat4float32numbersingle precision floating-point number (4 bytes)
serialserial4int32integerautoincrementing four-byte integer
smallintint2int16integersigned two-byte integer
smallserialserial2int16integerautoincrementing two-byte integer
text—utf8stringvariable-length character string
time with time zonetimetztimestamp_us_utcdatetime of day, including time zone
time without time zonetimetimestamp_us_utcdatetime of day (no time zone)
timestamp with time zonetimestamptztimestamp_us_utcdatedate and time, including time zone
timestamp without time zonetimestamptimestamp_us_utcdatedate and time (no time zone)
tsquery—utf8objecttext search query
tsrange—utf8stringrange of timestamp without time zone
tstzrange—utf8stringrange of timestamp with time zone
tsvector—utf8objecttext search document
txid_snapshot—binaryobjectuser-level transaction ID snapshot (deprecated; see pg_snapshot)
uuid—utf8stringuniversally unique identifier
varbitbit varyingutf8stringvariable-length bit string
varcharcharacter varyingutf8stringvariable-length character string
xml—utf8stringXML data

Troubleshooting  

Verify the Amfelt agent can reach the PostgreSQL host and port, and that the server’s listen_addresses and pg_hba.conf permit the agent’s network.
Confirm the vaulted username and password, and that the login role has CONNECT on the database.
The connector refuses a password that holds whitespace, a single quote ('), 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.
The PostgreSQL target needs PostgreSQL 15 or later. On an older server the connection test reports the server’s version and this floor, and a deploy fails with 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.
Delta and window loads compute their compare in a temporary table, so the role the connection acts as needs 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.
When a source sends a column as text and the target column is smallint, integer, bigint, oid, numeric, real, double precision, boolean, date, time, time with time zone, timestamp or timestamp with time zone, or a domain over one of them, the target checks each value first. From version 1.7.3 of the file connector on, a CSV column deployed as a number that now holds 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.
The role is missing SELECT (source) or write/DDL (target). Re-apply the grants in the Required privileges section, including ALTER DEFAULT PRIVILEGES for objects created later.
Set sslmode to require or verify-full in the vaulted credentials.
The first quarantined file of an object creates the schema <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.

PackageLicense
github.com/Masterminds/semver  MIT
github.com/apache/arrow-go/v18  Apache-2.0 AND BSD-3-Clause
github.com/apapsch/go-jsonmerge/v2  MIT
github.com/bmatcuk/doublestar/v4  MIT
github.com/fsnotify/fsnotify  BSD-3-Clause
github.com/gabriel-vasile/mimetype  MIT
github.com/go-cmd/cmd  MIT
github.com/go-playground/locales  MIT
github.com/go-playground/universal-translator  MIT
github.com/go-playground/validator  MIT
github.com/go-playground/validator/v10  MIT
github.com/go-viper/mapstructure/v2  MIT
github.com/goccy/go-json  MIT
github.com/goccy/go-yaml  MIT
github.com/google/flatbuffers  Apache-2.0
github.com/google/uuid  BSD-3-Clause
github.com/hashicorp/errwrap  MPL-2.0
github.com/hashicorp/go-multierror  MPL-2.0
github.com/iancoleman/strcase  MIT
github.com/jackc/pgpassfile  MIT
github.com/jackc/pgservicefile  MIT
github.com/jackc/pgx/v5  MIT
github.com/jackc/puddle/v2  MIT
github.com/jinzhu/copier  MIT
github.com/klauspost/compress  Apache-2.0 AND BSD-3-Clause AND MIT
github.com/klauspost/cpuid/v2  MIT
github.com/leodido/go-urn  MIT
github.com/lmittmann/tint  MIT
github.com/mattn/go-isatty  MIT
github.com/mitchellh/go-wordwrap  MIT
github.com/oapi-codegen/runtime  Apache-2.0
github.com/pelletier/go-toml/v2  MIT
github.com/pierrec/lz4/v4  BSD-3-Clause
github.com/sagikazarmark/locafero  MIT
github.com/santhosh-tekuri/jsonschema/v6  Apache-2.0
github.com/spf13/afero  Apache-2.0
github.com/spf13/cast  MIT
github.com/spf13/cobra  Apache-2.0
github.com/spf13/pflag  BSD-3-Clause
github.com/spf13/viper  MIT
github.com/subosito/gotenv  MIT
github.com/xo/dburl  MIT
github.com/zeebo/xxh3  BSD-2-Clause
go.yaml.in/yaml/v3  MIT
golang.org/x/crypto  BSD-3-Clause
golang.org/x/exp  BSD-3-Clause
golang.org/x/net  BSD-3-Clause
golang.org/x/sync  BSD-3-Clause
golang.org/x/sys  BSD-3-Clause
golang.org/x/text  BSD-3-Clause
google.golang.org/genproto/googleapis/rpc  Apache-2.0
google.golang.org/grpc  Apache-2.0
google.golang.org/protobuf  BSD-3-Clause