Skip to main content

Redshift connector

Extracts a Redshift table by UNLOAD to your own S3 bucket: the cluster writes UNLOAD ('SELECT <mapped columns> …') TO 's3://{bucket}/{prefix}/tessera/{sync_id}/part_' IAM_ROLE '{iam_role_arn}' FORMAT PARQUET PARALLEL ON MAXFILESIZE 256 MB; Tessera reads the produced Parquet objects back, ingests them, then deletes them. A deletion failure never fails the sync — it surfaces as a redshift_unload_cleanup_failed warning in the sync's details so you can clean the prefix yourself. Only mapped columns leave the cluster; incremental cursor literals are rendered server-side, never from raw user strings.

Prerequisites​

  • An API key with the admin scope.
  • An S3 bucket + prefix for unloads, in the cluster's region.
  • An IAM role attached to the cluster with s3:PutObject on the prefix (the UNLOAD writer).
  • Credentials Tessera reads the unloaded objects with: an access key pair holding s3:ListBucket, s3:GetObject, s3:DeleteObject on the same prefix.
  • A cluster endpoint with a public address. Tessera connects only to public internet addresses. config.host follows the Postgres connector's destination rules, and the port is 5439 or 1024–65535. A refused endpoint fails authenticate with failure: configuration. PUT /mapping returns 400 invalid_argument with a field_violation on config/host or config/port, and a sync fails with extraction_failed.

Create the credential (least privilege)​

A read-only database user:

CREATE USER tessera_reader PASSWORD '...';
GRANT USAGE ON SCHEMA ops TO tessera_reader;
GRANT SELECT ON ops.scan_events TO tessera_reader;

If your cluster restricts IAM role use (after REVOKE ASSUMEROLE … FROM PUBLIC), also run GRANT ASSUMEROLE ON 'arn:aws:iam::123456789012:role/redshift-tessera-unload' TO tessera_reader FOR UNLOAD;.

The unload IAM role policy (attached to the cluster):

{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::acme-exports/tessera-unloads/*"
}, {
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::acme-exports"
}]
}

The policy for the key pair Tessera reads the unloaded objects with — it lists, reads and deletes only Tessera's own unload prefix:

{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::acme-exports",
"Condition": { "StringLike": { "s3:prefix": ["tessera-unloads/tessera/*"] } }
}, {
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::acme-exports/tessera-unloads/tessera/*"
}]
}

With SSE-KMS on the bucket, the role also needs kms:GenerateDataKey and Tessera's key pair kms:Decrypt on that key.

Create the source​

{
"name": "scan-events",
"kind": "redshift",
"config": {
"host": "acme-cluster.abc123.us-east-1.redshift.amazonaws.com",
"port": 5439,
"database": "ops",
"schema": "ops",
"table": "scan_events",
"unload": {
"s3_bucket": "acme-exports",
"s3_prefix": "tessera-unloads",
"region": "us-east-1",
"iam_role_arn": "arn:aws:iam::123456789012:role/redshift-tessera-unload"
}
},
"credentials": {
"username": "tessera_reader",
"password": "...",
"unload_access_key_id": "AKIA...",
"unload_secret_access_key": "..."
}
}

The unload_* key pair is what Tessera reads the unloaded objects with; omit it to use the platform's ambient AWS identity (the same mechanics the S3 bucket connector uses). The config object validates against the connector's registered schema:

{
"host": "acme-cluster.abc123.us-east-1.redshift.amazonaws.com",
"database": "ops",
"schema": "ops",
"table": "scan_events",
"unload": {
"s3_bucket": "acme-exports",
"s3_prefix": "tessera-unloads",
"region": "us-east-1",
"iam_role_arn": "arn:aws:iam::123456789012:role/redshift-tessera-unload"
}
}

Test the connection​

POST /sources/{id}/connection-diagnostics runs three checks in order, each tagged with the diagnostic stage it verifies:

CheckStageVerifies
authenticateauthenticationthe database credentials connect and run a probe
table_readableschemathe table exists and its schema is readable
unload_probeauthorizationa full round-trip: UNLOAD ('SELECT 1') to the configured prefix, read back through Tessera's S3 path, then deleted — proving both the IAM role and the read credentials

A failed check carries a typed failure cause and a detail naming the fix for the phase that failed: the cluster's UNLOAD (IAM role and its s3:PutObject), Tessera's read (s3:ListBucket, s3:GetObject) or the cleanup (s3:DeleteObject); later checks report not_attempted. Details never repeat Redshift's or AWS's message or any credential. The response's checked_at is the server time of the test, which is also recorded in the audit log as source.test_connection. POST /sources/{id}/test-connection runs the same checks and returns only their names, statuses and details.

Rotate the credential​

The source holds the database user's password and, optionally, the unload_* access key pair that Tessera reads the unloaded objects with. Both are long-lived; Tessera does not accept a session token for the key pair. To rotate, create the new password or key, then PATCH /sources/{id} with credentials. The body replaces the stored credentials entirely, so repeat every field you still use, and send the two halves of the key pair together. Confirm POST /sources/{id}/connection-diagnostics passes, then retire the old credential. The IAM role in unload.iam_role_arn is the role Redshift assumes for UNLOAD; it is part of the source config, not a credential Tessera holds.

Type map​

Column types map onto canonical attribute types exactly as follows; mapping a column whose type is unsupported fails PUT /sources/{id}/mapping naming the column and its type. Redshift has no array type — no column maps to array.

Redshift typeCanonical typeNotes
smallintintegerExact integers (INT2).
integerintegerExact integers (INT, INT4).
bigintintegerExact 64-bit integers (INT8).
decimalnumberNUMERIC — converted to float64 (beyond-precision values convert lossily). An entity or event id column keeps its exact decimal value.
realnumberIEEE-754 floats (FLOAT4).
double precisionnumberIEEE-754 float64 (FLOAT, FLOAT8), as-is.
booleanbooleanBooleans, as-is (BOOL).
charstringFixed-length strings, as returned by Redshift (CHARACTER, NCHAR, BPCHAR).
varcharstringUTF-8 strings (CHARACTER VARYING, NVARCHAR, TEXT), ≤ 4096 chars per 002 §2.
timestamptztimestampTIMESTAMP WITH TIME ZONE — converted to RFC 3339 UTC.
timestamptimestampTIMESTAMP WITHOUT TIME ZONE — naive wall-clock time: the mapping's IANA timezone is required; without it, rows carrying a value are rejected.
datetimestampMidnight UTC of the date (YYYY-MM-DDT00:00:00Z).
superunsupportedCannot be mapped (flatten upstream); PUT /mapping fails naming the column.
varbyteunsupportedCannot be mapped; PUT /mapping fails naming the column.
geometryunsupportedCannot be mapped; PUT /mapping fails naming the column.
geographyunsupportedCannot be mapped; PUT /mapping fails naming the column.
hllsketchunsupportedCannot be mapped; PUT /mapping fails naming the column.
intervalunsupportedCannot be mapped; PUT /mapping fails naming the column.

DATE maps to midnight UTC by convention; if your dates carry local-day semantics, expose a naive TIMESTAMP column instead and set the mapping timezone.

Worked example​

A logistics table ops.scan_events unloaded nightly maps as: entity {constant: "shipment", id_column: "shipment_id"}, event type constant shipment.scanned, timestamp scanned_at, attributes hub_code, weight_kg, temp_c, cursor scanned_at.