# v0.69.0 — DuckLake Sink Reliability & Security

> **Full details:** [v0.69.0.md-full.md](v0.69.0.md-full.md)

## What's New

v0.69.0 transforms the DuckLake sink from a best-effort post-refresh side effect
into a trustworthy, observable, and secure delivery mechanism. It resolves five
HIGH- and MEDIUM-severity findings from Assessment 13 that all cluster around
the DuckLake integration surface: sink delivery semantics, view registration
staleness, snapshot-ID race condition, unqualified schema resolution, and missing
observability.

### ARCH-002 / REL-001: DuckLake Sink Delivery State Machine (ARCH-002, REL-001)

The DuckLake sink now has durable delivery semantics. A new catalog table,
`pgtrickle.pgt_ducklake_sink_delivery`, tracks each write attempt with columns:
`delivery_id`, `stream_table_id`, `refresh_id`, `status`
(`PENDING` | `WRITING` | `DELIVERED` | `FAILED_RETRYABLE` | `FAILED_PERMANENT`),
`attempt_count`, `bytes_written`, `rows_written`, `started_at`, `finished_at`,
`last_error`.

`run_ducklake_sink()` is rewritten as a state-machine driver:
1. Inserts a `PENDING` row before attempting any S3 write.
2. Transitions to `WRITING` before the Parquet upload.
3. On success, transitions to `DELIVERED` and updates byte/row counts.
4. On retryable failure (network timeout, transient S3 error), transitions to
   `FAILED_RETRYABLE` and increments `attempt_count`; the scheduler will retry
   on the next tick up to `pg_trickle.ducklake_sink_max_retries` (default 3).
5. On permanent failure, transitions to `FAILED_PERMANENT` and does not retry.

A new `pg_trickle.ducklake_sink_failure_mode` GUC (`warn` | `error`, default
`warn`) controls whether a permanent failure propagates as an error to the
calling refresh or remains asynchronous.

### COR-005: DuckLake View Registration After Query-Only ALTER (COR-005)

`alter_stream_table()` now detects when a stream table has an active
`ducklake_sink_mode` and the incoming ALTER contains only a `query` change.
In that case, the DuckLake view row is updated with the new query definition
immediately after the pg_trickle metadata migration, without requiring the
operator to also re-specify sink parameters.

### COR-006: DuckLake Snapshot ID Serialization (COR-006)

`register_ducklake_data_file()` and the related snapshot catalog writes now
acquire `pg_advisory_xact_lock(hashtext('ducklake_sink_' || table_id::text))`
before any `MAX(snapshot_id) + 1` computation. This eliminates the race window
where two concurrent sink writers for the same DuckLake table could compute the
same next snapshot ID.

### SEC-002: Qualified DuckLake Catalog Resolution (SEC-002)

All DuckLake catalog SQL in `ducklake_sink.rs` now resolves the DuckLake schema
explicitly. A new `pg_trickle.ducklake_catalog_schema` GUC (default `main`)
holds the schema name. `ducklake_view_table_exists()` queries `pg_class` and
`pg_namespace` with the qualified schema rather than using
`information_schema.tables` with only `table_name`. All `INSERT`/`UPDATE`
statements use `quote_ident(schema) || '.' || quote_ident(table)` patterns via
the shared SQL builder.

### OBS-001: DuckLake Sink Health Metrics (OBS-001)

New SQL function `pgtrickle.ducklake_sink_status()` returns a set of rows — one
per stream table with an active DuckLake sink — with columns:
`stream_table_name`, `last_delivery_status`, `last_delivery_at`,
`last_bytes_written`, `last_rows_written`, `failed_attempts`, `last_error`.

Prometheus counters added:
- `pg_trickle_ducklake_sink_attempts_total{stream_table}`
- `pg_trickle_ducklake_sink_successes_total{stream_table}`
- `pg_trickle_ducklake_sink_failures_total{stream_table,kind}`
- `pg_trickle_ducklake_sink_bytes_written_total{stream_table}`
- `pg_trickle_ducklake_sink_upload_seconds{stream_table}` (histogram)

### DEP-002: Arrow/Parquet/ObjectStore Dependency Policy (DEP-002)

`Cargo.toml` gains structured comments for the DuckLake dependency group
documenting the upgrade cadence (major versions on DuckLake protocol changes,
minor versions for CVE remediation within 30 days), supported object-store
schemes (`s3`, `gcs`, `az`), the binary-size impact (approx. 12 MB stripped),
and the CI coverage requirement (E2E DuckLake tests on every PR).
`deny.toml` includes the Arrow/Parquet dependency group in the
version-lock advisory section.

## Breaking Changes

- `pg_trickle.ducklake_sink_failure_mode = 'error'` causes sink failures to
  abort the refresh. The default remains `'warn'` (backward-compatible).
- The `ducklake_catalog_schema` GUC defaults to `main`. Deployments where
  DuckLake uses a different schema must set this GUC explicitly.
