Dev-iL commented on issue #68453:
URL: https://github.com/apache/airflow/issues/68453#issuecomment-4887631062
### Manifest
~~~md
# Definition: Migrate the sync Postgres driver from psycopg2 to psycopg3
(apache/airflow#68453)
## 1. Intent & Context
- **Goal:** Close apache/airflow#68453 — make psycopg3 the single default
Postgres driver for both sync and async, and remove `psycopg2-binary` as a hard
dependency anywhere in the repo, mirroring the async-side precedent
(#67801/#68496/#69089) exactly where it applies.
- **Mental Model:**
- Two independent Postgres driver questions exist in this codebase: the
**metadata engine** (`sql_alchemy_conn`, owned by `airflow-core`, used by the
scheduler/ORM) and **`PostgresHook`** (`providers/postgres`, used by task
operators to reach arbitrary user Postgres databases). This issue touches both,
plus several *other* provider packages that independently hardcode or
transitively depend on psycopg2 for reasons unrelated to `PostgresHook` itself.
- The async default switch (#67801 core, #69089 provider) is the direct
precedent for every decision here: same "no implicit schemes, fail loudly"
philosophy, same hard-dep↔extra flip mechanics, same changelog-only (no
newsfragment) treatment for providers, same core-vs-provider PR split.
- A repo-wide grep sweep (this session) found the blast radius is wider
than the issue text: `providers/celery`, `providers/google`, and
`providers/pgvector` each have unconditional psycopg2 dependencies that would
break once `providers/postgres` stops guaranteeing psycopg2 is installed. All
three are in scope.
- Item 4 of the issue (bulk-write validation) cannot be executed in this
environment (no resources to run breeze/Postgres) — its *artifacts* (a
benchmark script + a spec for extending the user's external `Dev-iL/pgbench`
tool) are this session's deliverable; the actual benchmark *run*, and the final
go/no-go call it feeds, happen later on a separate system.
- Full evidence trail:
`~/.manifest-dev/logs/figure-out-log-20260705-192456.md`.
## 2. Initial Approach
- **Architecture:** Five independent, narrowly-scoped changes rather than
one monolithic change — mirrors how the async work was split into a core PR
(#68496) and a provider PR (#69089), extended here to one PR per affected
package for release-manager friendliness (per explicit user decision).
- **Execution Order:**
- D1 (airflow-core) → D2 (providers/postgres); D3 (providers/celery), D4
(providers/google), D5 (providers/pgvector), and D6 (benchmark artifacts) can
all proceed in parallel with each other and largely in parallel with D2.
- Rationale: D1 establishes the actual driver-detection/default logic
every other deliverable either consumes or independently mirrors. D2–D5 are
independent leaf packages that only need to stop hardcoding/hard-depending on
psycopg2 — each is self-contained and separately verifiable. D6 has no code
dependency on the others.
- **Risk Areas:**
- [R-1] The deferred benchmark (D6) could later show psycopg3 is
unacceptably slower for real bulk-write paths, contradicting the D1/D2 default
flip. | Detect: the user's benchmark run on their other system; a
markedly-slower result reopens D1's design (see "what would overturn this"
below).
- [R-2] Undiscovered psycopg2 consumers beyond the five found via this
session's grep sweep (`providers/postgres`, `providers/celery`,
`providers/google`, `providers/pgvector`, plus benign comment/mock-import hits
in `airflow-core/src/airflow/utils/sqlalchemy.py` and
`devel-common/src/docs/utils/conf_constants.py` that need no change). | Detect:
re-run the repo-wide grep in INV-G-BLAST-RADIUS as a final check before
considering the manifest done.
- [R-3] `uv.lock` / constraints drift after each `pyproject.toml` change
(D2). | Detect: `uv lock` diff review; CI constraint-update check.
- [R-4] pgvector's fix (D5) could be mistaken for a full fix of that
provider's psycopg2-vs-psycopg3 connection-type bug. | Detect: keep the
tracking-issue scope explicit in the code comment and PR description so
reviewers don't expect more than the import-guard here.
- **Trade-offs:**
- [T-1] Fail loudly vs. auto-remap when psycopg2 is missing for an
explicit `+psycopg2` URL → **fail loudly** (explicit user decision: "no
implicit schemes in the project").
- [T-2] Delete psycopg2 support from `PostgresHook` vs. keep it guarded →
**keep, guarded** — preserves compatibility down to the provider's declared
`apache-airflow>=2.11.0` floor (which may still run SQLAlchemy 1.4).
- [T-3] Auto-push/open PRs vs. stop at ready-for-PR → **stop** (explicit
user decision) — the deferred benchmark (D6) gates actual submission, so /do
halts once each package is self-reviewed and its checks are green.
- [T-4] Bundle celery/google/pgvector fixes into this manifest vs. defer
to separate issues → **bundle** (explicit user decision) — they're mechanical,
directly-caused consequences of the same migration; pgvector's *deeper*
pre-existing bug still gets its own tracking issue rather than a full fix here.
- [T-5] One combined PR vs. one per affected package → **one per package**
(explicit user decision, mirrors the async core/provider split).
## 3. Global Invariants
- [INV-G1] `change-intent` review dimension: no LOW+ findings across the
full diff.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=change-intent and review the full diff across all deliverables
(airflow-core, providers/postgres, providers/celery, providers/google,
providers/pgvector, dev/ benchmark script). PASS only if no LOW-or-higher
findings."
phase: 2
```
- [INV-G2] `code-bugs` review dimension: no LOW+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-bugs and review the full diff across all deliverables. PASS only
if no LOW-or-higher findings."
phase: 2
```
- [INV-G3] `contracts` review dimension (applies — this changes public
dependency contracts in multiple `pyproject.toml` files, a durable
config-upgrade contract in `configuration.py`, and public docstrings/behavior
in `PostgresHook`): no LOW+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=contracts and review the full diff, paying particular attention to
providers/postgres/pyproject.toml, providers/celery, providers/google, and
providers/pgvector dependency/behavior changes. PASS only if no LOW-or-higher
findings."
phase: 2
```
- [INV-G4] `type-safety` review dimension (Python with type hints
throughout): no LOW+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=type-safety and review the full diff. PASS only if no LOW-or-higher
findings."
phase: 2
```
- [INV-G5] `operational-readiness` review dimension: no MEDIUM+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=operational-readiness and review the full diff. PASS only if no
MEDIUM-or-higher findings."
phase: 2
```
- [INV-G6] `code-maintainability` review dimension: no MEDIUM+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-maintainability and review the full diff. PASS only if no
MEDIUM-or-higher findings."
phase: 2
```
- [INV-G7] `code-simplicity` review dimension: no MEDIUM+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-simplicity and review the full diff. PASS only if no
MEDIUM-or-higher findings."
phase: 2
```
- [INV-G8] `test-quality` review dimension: no MEDIUM+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=test-quality and review the full diff. PASS only if no
MEDIUM-or-higher findings."
phase: 2
```
- [INV-G9] `code-testability` review dimension: no MEDIUM+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-testability and review the full diff. PASS only if no
MEDIUM-or-higher findings."
phase: 2
```
- [INV-G10] `docs` review dimension: no MEDIUM+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with dimension=docs
and review the full diff, including set-up-database.rst and all changelog.rst
edits. PASS only if no MEDIUM-or-higher findings."
phase: 2
```
- [INV-G11] `context-file-adherence` review dimension (CLAUDE.md conformance
— newsfragment rules, exception-type rules, commit message style,
PR-per-package split): no MEDIUM+ findings.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=context-file-adherence and review the full diff against this repo's
CLAUDE.md, in particular: no new raise AirflowException usages; newsfragment
only added for airflow-core's user-facing change, never for providers/*; commit
messages use imperative mood with no Conventional Commits prefixes; the
tracking-issue-plus-code-comment pattern was followed for the pgvector deferred
bug. PASS only if no MEDIUM-or-higher findings."
phase: 2
```
- [INV-G12] Project static checks pass across every touched file.
```yaml
verify:
prompt: "Run `prek run --from-ref main --stage pre-commit` and `prek run
--from-ref main --stage manual` from the repo root. PASS only if both complete
with no failures on files touched by this manifest
(airflow-core/src/airflow/configuration.py,
airflow-core/src/airflow/settings.py,
airflow-core/docs/howto/set-up-database.rst, providers/postgres/**,
providers/celery/**, providers/google/**, providers/pgvector/**, dev/**).
Report any failure verbatim."
phase: 3
```
- [INV-G13] `mypy-airflow-core` hook passes for the touched airflow-core
files.
```yaml
verify:
prompt: "Run `prek run mypy-airflow-core --all-files` (or targeted
equivalent) and confirm it passes for airflow-core/src/airflow/configuration.py
and airflow-core/src/airflow/settings.py. PASS only if there are no new mypy
errors introduced by this manifest's changes."
phase: 3
```
- [INV-G14] No new unconditional/module-top-level `psycopg2` (or
`pgvector.psycopg2`) imports remain anywhere in the repo outside what this
manifest deliberately keeps guarded.
```yaml
verify:
prompt: "Run `grep -rln '^import psycopg2\\|^from psycopg2\\|from
pgvector.psycopg2' --include='*.py' .` from the repo root, excluding .venv/,
/tests/, /test_ paths. For every remaining hit, confirm the import is
lazy/guarded (inside a function or a try/except, not at module top level) or is
a documented, deliberate exception. PASS only if no unconditional module-level
psycopg2-family import remains anywhere in source code."
phase: 3
```
- [INV-G15] Newsfragment discipline: `airflow-core` gets exactly one
newsfragment for this user-facing change; no newsfragment exists anywhere under
`providers/*/newsfragments/` for this work (providers/postgres,
providers/celery, providers/google, providers/pgvector all use their
`docs/changelog.rst` directly instead, per CLAUDE.md).
```yaml
verify:
prompt: "Check that airflow-core/newsfragments/ contains exactly one new
.rst file for this change (any of .improvement/.feature/.bugfix/.misc is
acceptable — judge fit against the actual change) and that no new files were
added under providers/postgres/newsfragments/, providers/celery/newsfragments/,
providers/google/newsfragments/, or providers/pgvector/newsfragments/ (none of
these directories should even exist as targets of new files). PASS only if both
conditions hold."
phase: 3
```
## 4. Process Guidance
- [PG-1] Do not push any branch to `origin` or open any PR for any of the
five packages touched here — that is a manual step the user takes after the
deferred benchmark (D6) comes back from their other system and confirms it's
safe to ship the default flip.
- [PG-2] Do not attempt to actually run the Postgres benchmark, spin up
breeze/Docker Postgres, or push changes to the external `Dev-iL/pgbench`
repository from this workspace — produce the spec and the `dev/` script only;
the run itself happens elsewhere.
- [PG-3] No ADRs for any decision in this work. This repo's own precedent
for this exact class of decision (the async driver default switch) was recorded
via issue body + changelog note + PR description only — follow the same
convention.
- [PG-4] Do not bump any `apache-airflow` / SQLAlchemy minimum-version floor
anywhere, and do not introduce a gradual-deprecation warning period — this
mirrors the async precedent's immediate-flip treatment exactly.
- [PG-5] Organize the work into five separate, self-contained diffs/branches
mirroring the async precedent's core/provider split: one for `airflow-core`
(D1), and one each for `providers/postgres` (D2), `providers/celery` (D3),
`providers/google` (D4), and `providers/pgvector` (D5). Do not bundle unrelated
packages' changes into a single commit.
- [PG-6] For `providers/pgvector` (D5): open a GitHub tracking issue for the
separate pre-existing bug (the operator calls the psycopg2-specific
`pgvector.psycopg2.register_vector` regardless of whether `PostgresHook` is
actually using psycopg2 or psycopg3 — likely wrong in the common psycopg3 case)
*before* finalizing that deliverable, per CLAUDE.md's "Tracking issues for
deferred work" process — then add the full tracking-issue URL as a code comment
at the guarded-import site. Do not use vague forward-looking language in its
place.
- [PG-7] This environment may not be able to run breeze or a live Postgres
instance. Run whatever pure-Python/unit-level tests are feasible via `uv run
--project <PROJECT> pytest ...` (CLAUDE.md's documented breeze-unavailable
fallback). Anything genuinely requiring a live Postgres instance (D6's actual
benchmark execution, and any Postgres-backed integration tests touching D1–D5)
is validated by the user separately, outside this manifest's automated
verification.
## 5. Known Assumptions
- [ASM-1] Deliverable-to-package grouping (airflow-core / postgres / celery
/ google / pgvector — five groups) matches the user's "split core vs providers,
like the async case" instruction; exact branch names are left to the
implementer. | Default: five branches named by convention (e.g.
`.../psycopg3-sync-core`, `.../psycopg3-sync-postgres-provider`, etc.) | Impact
if wrong: cosmetic — nothing is pushed by this manifest, so regrouping later is
free.
- [ASM-2] D6's benchmark artifacts are verified via code/structural review
plus a non-Postgres smoke test, not live execution against real Postgres,
because this workspace lacks the resources to run breeze/Postgres. | Default:
AC-6.1 accepts a script proven correct by review + a SQLite/mocked-engine smoke
run. | Impact if wrong: a latent bug in the `dev/` script surfaces only when
the user runs it for real later — acceptable, since that run is the actual
validation gate regardless.
- [ASM-3] The airflow-core newsfragment's exact classification
(`.improvement.rst` vs `.misc.rst` vs another type) is left to the
implementer's judgment at authoring time. | Default: `.improvement.rst`, since
this changes default behavior without adding a new feature or fixing a defect.
| Impact if wrong: trivial, a maintainer or reviewer will request a
reclassification.
- [ASM-4] pgvector's fix (D5) is a lazy-import + clear-error guard, not a
new hard/extra dependency declaration in `providers/pgvector/pyproject.toml`. |
Default: guard via `try/except ImportError` inside `_register_vector`, raising
`AirflowOptionalProviderFeatureException` with an actionable message if
`pgvector.psycopg2` (or psycopg2 itself) is unavailable. | Impact if wrong: if
reviewers prefer an explicit dependency declaration instead, that's a small,
low-risk pyproject.toml addition during review.
- [ASM-5] `providers/celery`'s result-backend URL default change is treated
as user-facing and gets a changelog "Breaking changes" note (same reasoning as
the core change); `providers/google`'s adapter-registration fix is treated as a
pure bugfix with no behavior change for working configurations and does **not**
get a breaking-change note. | Impact if wrong: a maintainer may ask for the
classification to be flipped for one or the other during review — low-cost to
adjust.
## 6. Deliverables
### Deliverable 1: airflow-core — sync driver default & bulk-tuning
correctness
**Acceptance Criteria:**
- [AC-1.1] `_upgrade_postgres_metastore_conn`
(`airflow-core/src/airflow/configuration.py`) has its `good_scheme` constant
changed from `"postgresql+psycopg2"` to `"postgresql+psycopg"`. The
`bad_schemes` list is unchanged (`["postgres+psycopg2", "postgres",
"postgresql"]`) — no new environment-probing/remap logic is added, and an
explicit `postgresql+psycopg2://` URL is never rewritten.
`airflow-core/tests/unit/core/test_configuration.py::test_upgrade_postgres_metastore_conn`
is updated to assert the new target scheme, plus a new case confirming an
explicit `postgresql+psycopg2://` value is left untouched.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-bugs and review airflow-core/src/airflow/configuration.py's
_upgrade_postgres_metastore_conn and its test coverage in
test_configuration.py. Confirm: good_scheme is now 'postgresql+psycopg';
bad_schemes list is unchanged; an explicit postgresql+psycopg2:// input is
never rewritten by this function (verify via a passing test case, not just
inspection). PASS only if all three hold and no LOW-or-higher findings."
phase: 2
```
- [AC-1.2] `prepare_engine_args` (`airflow-core/src/airflow/settings.py`)
decides whether to apply psycopg2-specific
`executemany_mode`/`executemany_batch_page_size` tuning based on the **actual
configured** `SQL_ALCHEMY_CONN` scheme (e.g. does it start with
`postgresql+psycopg2`), not merely whether the `psycopg` package is importable.
New unit tests cover both a `postgresql+psycopg2://`-configured case (tuning
applied) and a `postgresql+psycopg://`-configured case (tuning skipped),
independent of what happens to be importable in the test environment.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-bugs and review airflow-core/src/airflow/settings.py's
prepare_engine_args and its _USE_PSYCOPG3-derived logic. Confirm the
psycopg2-specific executemany tuning is gated on the actually-configured
SQL_ALCHEMY_CONN scheme, not on psycopg package importability, and that this is
covered by a test that would fail against the old (importability-only) logic.
PASS only if confirmed and no LOW-or-higher findings."
phase: 2
```
- [AC-1.3] Behavioral exercise: running configuration loading and
`prepare_engine_args()` locally (no live Postgres required) against
representative `SQL_ALCHEMY_CONN` values — bare `postgresql://`, explicit
`postgresql+psycopg2://`, explicit `postgresql+psycopg://` — produces the
correct resulting scheme and correct `engine_args` in each case.
```yaml
verify:
prompt: "Run a short Python snippet (via `uv run --project airflow-core
python -c ...` or equivalent) that loads AirflowConfigParser with each of: bare
'postgresql://...', explicit 'postgresql+psycopg2://...', explicit
'postgresql+psycopg://...' as sql_alchemy_conn, and separately calls
prepare_engine_args() against each resulting SQL_ALCHEMY_CONN value. Confirm:
the bare/legacy case ends up as postgresql+psycopg; the explicit +psycopg2 case
is untouched and gets executemany tuning; the explicit +psycopg case is
untouched and does not get executemany tuning. PASS only if all three behave as
described. Report BLOCKED if this requires infrastructure unavailable in this
environment (it should not, since no live Postgres connection is needed for
config-parsing/dialect-detection logic)."
phase: 2
```
- [AC-1.4] `airflow-core/docs/howto/set-up-database.rst` gains a sync-side
section mirroring the existing async auto-derivation section (~lines 210–220),
describing psycopg3 as the default sync driver and pointing to the
providers/postgres `[psycopg2]` extra for anyone who needs the old driver.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with dimension=docs
and review the new sync-driver section added to
airflow-core/docs/howto/set-up-database.rst. Confirm it parallels the existing
async section's structure and correctly documents: psycopg3 as the new default,
how the [psycopg2] extra restores the old driver, and what happens if a config
still has an explicit postgresql+psycopg2:// URL without psycopg2 installed.
PASS only if no MEDIUM-or-higher findings."
phase: 2
```
- [AC-1.5] A newsfragment is added under `airflow-core/newsfragments/`
describing this user-facing default change.
```yaml
verify:
prompt: "Confirm exactly one new .rst file exists under
airflow-core/newsfragments/ describing the sync Postgres driver default change,
following the existing newsfragment format/conventions in that directory. PASS
if present and well-formed."
phase: 3
```
### Deliverable 2: providers/postgres — dependency flip & hook guard
**Acceptance Criteria:**
- [AC-2.1] `providers/postgres/pyproject.toml`: `psycopg2-binary` is removed
from the main `dependencies` array and added as a new `"psycopg2"`
optional-dependencies extra with its existing version constraints (`>=2.9.9` /
`>=2.9.10` python-version split) preserved. The `apache-airflow>=2.11.0` floor
and the `"sqlalchemy"` extra pin are unchanged. `prek run
update-providers-dependencies --all-files` has been run and `uv.lock`
regenerated/committed.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=contracts and review the diff to providers/postgres/pyproject.toml.
Confirm: psycopg2-binary moved from dependencies to a new 'psycopg2' extra with
its version constraints preserved; apache-airflow floor and 'sqlalchemy' extra
unchanged; uv.lock reflects the change. PASS only if all hold and no
LOW-or-higher findings."
phase: 2
```
- [AC-2.2]
`providers/postgres/src/airflow/providers/postgres/hooks/postgres.py`'s
module-level `from psycopg2 import connect as ppg2_connect` and `from
psycopg2.extras import DictCursor, NamedTupleCursor, RealDictCursor,
execute_values` are made lazy/guarded (moved inside the functions/branches that
need them, following the existing try/except pattern already used for the
psycopg3 import a few lines below). The module imports successfully even when
psycopg2 is not installed. `USE_PSYCOPG3 = is_sqla2` dual-path logic is
otherwise structurally unchanged.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-bugs and review
providers/postgres/src/airflow/providers/postgres/hooks/postgres.py. Confirm
the psycopg2 imports are no longer unconditional at module top level, the
module can be imported with psycopg2 absent, and USE_PSYCOPG3's is_sqla2-based
logic is preserved (not redesigned). PASS only if confirmed and no
LOW-or-higher findings."
phase: 2
```
- [AC-2.3] Behavioral exercise: importing
`airflow.providers.postgres.hooks.postgres` succeeds in an environment where
`psycopg2` is uninstalled/mocked as absent (e.g. via `sys.modules` patching or
a dedicated venv), and the psycopg2-specific code path (when reached) raises a
clear error rather than an import-time crash.
```yaml
verify:
prompt: "Write and run a test (or use an existing one if added) that
simulates psycopg2 being absent (e.g. patch sys.modules['psycopg2'] = None or
use importlib machinery) and confirms `import
airflow.providers.postgres.hooks.postgres` still succeeds. PASS only if import
succeeds under simulated absence and any subsequent attempt to actually use the
psycopg2 path fails with a clear, actionable error rather than a bare traceback
at import time."
phase: 2
```
- [AC-2.4] `providers/postgres/docs/changelog.rst` gains a "Breaking
changes" note for the sync driver default change and the new `[psycopg2]`
extra, in the same style and location as the existing async breaking-change
note (added by #69089).
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with dimension=docs
and review the new entry in providers/postgres/docs/changelog.rst. Confirm it
sits in the same 'Breaking changes' section as the existing async note,
describes the sync default change to psycopg3 and the psycopg2-binary-to-extra
move, and gives users the exact remediation ('install
apache-airflow-providers-postgres[psycopg2] to keep using psycopg2'). PASS only
if no MEDIUM-or-higher findings."
phase: 2
```
### Deliverable 3: providers/celery — result-backend URL scheme fix
**Acceptance Criteria:**
- [AC-3.1]
`providers/celery/src/airflow/providers/celery/executors/default_celery.py`'s
hardcoded `sql_alchemy_conn.replace("postgresql://", "postgresql+psycopg2://",
1)` is updated to target `"postgresql+psycopg://"` instead, matching the new
core default, with the adjacent comment updated to reflect the new rationale.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-bugs and review
providers/celery/src/airflow/providers/celery/executors/default_celery.py's
result-backend URL derivation. Confirm the hardcoded replacement target changed
from postgresql+psycopg2:// to postgresql+psycopg://, the explanatory comment
was updated to match, and this is covered by an updated/new test asserting the
new scheme in the derived result-backend URL. PASS only if confirmed and no
LOW-or-higher findings."
phase: 2
```
- [AC-3.2]
`providers/celery/src/airflow/providers/celery/get_provider_info.py`'s example
connection string is updated from `db+postgresql+psycopg2://...` to
`db+postgresql+psycopg://...`.
```yaml
verify:
prompt: "Confirm
providers/celery/src/airflow/providers/celery/get_provider_info.py's example
Postgres connection string uses postgresql+psycopg instead of
postgresql+psycopg2. PASS if updated."
phase: 2
```
- [AC-3.3] Existing celery executor tests covering the result-backend URL
derivation are updated to assert the new default scheme.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=test-quality and confirm the celery executor's tests for
result-backend URL derivation (search providers/celery/tests or
providers/celery/tests_unit for default_celery-related tests) assert
postgresql+psycopg, not postgresql+psycopg2, as the derived scheme. PASS only
if the tests would fail against the pre-change code and no MEDIUM-or-higher
findings."
phase: 2
```
- [AC-3.4] `providers/celery/docs/changelog.rst` gains a note describing
this default-URL-derivation change as user-facing (per ASM-5, treated as
breaking-adjacent — same reasoning as the core change), unless the
implementer's review of the actual celery provider release conventions
concludes otherwise (documented if so).
```yaml
verify:
prompt: "Check providers/celery/docs/changelog.rst for a new note
describing the result-backend URL default change (postgresql+psycopg2 ->
postgresql+psycopg). If absent, confirm the implementer left a clear rationale
(e.g. in the PR description or a commit note reachable from this diff) for why
it was judged non-user-facing. PASS if either a changelog note exists or a
documented rationale for omitting it is present."
phase: 2
```
### Deliverable 4: providers/google — bigquery_to_postgres
adapter-registration guard
**Acceptance Criteria:**
- [AC-4.1]
`providers/google/src/airflow/providers/google/cloud/transfers/bigquery_to_postgres.py`'s
unconditional `from psycopg2.extensions import register_adapter` / `from
psycopg2.extras import Json` and the `register_adapter(...)` calls in
`postgres_hook` are made conditional — only imported/executed when the resolved
`PostgresHook` is actually on its psycopg2 path (`not
PostgresHook.USE_PSYCOPG3`) — so the module imports cleanly without psycopg2
installed and the (currently always-run, psycopg3-no-op) registration only
happens when it's meaningful.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-bugs and review
providers/google/src/airflow/providers/google/cloud/transfers/bigquery_to_postgres.py.
Confirm: the psycopg2 imports are no longer unconditional at module top level;
register_adapter is only invoked when PostgresHook is actually on the psycopg2
path; the module imports successfully with psycopg2 absent. PASS only if
confirmed and no LOW-or-higher findings."
phase: 2
```
- [AC-4.2] Existing `bigquery_to_postgres` tests still pass, and a
new/updated test confirms the module imports successfully when psycopg2 is
simulated as absent.
```yaml
verify:
prompt: "Run providers/google's bigquery_to_postgres test suite (e.g.
`uv run --project providers/google pytest
providers/google/tests/.../test_bigquery_to_postgres.py -xvs` or breeze
equivalent) and confirm all pass, including a case that simulates psycopg2
being absent and confirms successful import. PASS only if all tests pass."
phase: 3
```
### Deliverable 5: providers/pgvector — import-guard fix + tracking issue
for the deeper bug
**Acceptance Criteria:**
- [AC-5.1]
`providers/pgvector/src/airflow/providers/pgvector/operators/pgvector.py`'s
unconditional `from pgvector.psycopg2 import register_vector` is moved inside
`_register_vector()` (or equivalent lazy point), guarded with a clear,
actionable error (e.g. `AirflowOptionalProviderFeatureException`) if
`pgvector.psycopg2`/psycopg2 is unavailable, so the module imports cleanly
without psycopg2 installed.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-bugs and review
providers/pgvector/src/airflow/providers/pgvector/operators/pgvector.py.
Confirm the pgvector.psycopg2 import is no longer unconditional at module top
level, the module imports successfully with psycopg2 absent, and the guard
raises a clear actionable error (not a bare ImportError) when
psycopg2/pgvector.psycopg2 is genuinely needed but missing. PASS only if
confirmed and no LOW-or-higher findings."
phase: 2
```
- [AC-5.2] A GitHub tracking issue exists (opened per CLAUDE.md's "Tracking
issues for deferred work" process) documenting the separate pre-existing bug —
this operator calls the psycopg2-specific `register_vector` regardless of
whether the underlying `PostgresHook` connection is actually psycopg2 or
psycopg3 — and its full URL is added as a code comment at the guard site in
`pgvector.py`.
```yaml
verify:
prompt: "Confirm a GitHub issue exists in apache/airflow describing the
pgvector operator's psycopg2-vs-psycopg3 connection-type mismatch bug, and that
providers/pgvector/src/airflow/providers/pgvector/operators/pgvector.py
contains a code comment with that issue's full URL (not a bare #NNNNN
reference) at the guarded-import site. PASS only if both exist."
phase: 3
```
### Deliverable 6: Bulk-write validation artifacts (benchmark, execution
deferred)
**Acceptance Criteria:**
- [AC-6.1] A runnable script exists under `dev/` that connects via both
`postgresql+psycopg2://` and `postgresql+psycopg://`, performs a bulk INSERT
through SQLAlchemy's `insertmanyvalues` path at
`insertmanyvalues_page_size=10000` (matching `prepare_engine_args`'s configured
value), and reports comparable timing/throughput for both drivers. Verified via
code review (this environment cannot run it against live Postgres) plus a smoke
check that it fails clearly and immediately if no Postgres connection is
reachable, rather than hanging or crashing unhelpfully.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with
dimension=code-bugs and review the new dev/ benchmark script. Confirm it:
exercises SQLAlchemy's real insertmanyvalues path (not a hand-rolled loop) at
page_size=10000 matching airflow-core/src/airflow/settings.py's
prepare_engine_args configuration; runs the same operation against both
postgresql+psycopg2:// and postgresql+psycopg:// for a fair comparison; fails
with a clear, immediate, actionable message if it cannot reach a Postgres
instance (do not require an actual live run in this environment — that is
deferred). PASS only if confirmed and no LOW-or-higher findings."
phase: 2
```
- [AC-6.2] A self-contained written spec (e.g.
`dev/benchmark-postgres-sync-driver-spec.md`) precisely describes the batch
INSERT/COPY comparison scenario to add to the external `Dev-iL/pgbench`
repository, detailed enough to implement without further context from this
session.
```yaml
verify:
prompt: "Activate the manifest-dev:review-code skill with dimension=docs
and review the written benchmark spec. Confirm it is self-contained: names the
exact scenario (batch INSERT and COPY, sync psycopg2 vs sync psycopg3), the
methodology to mirror from the existing async psycopg3-vs-asyncpg comparison
cited in apache/airflow#67801, and what output/metrics would constitute a clear
go/no-go signal. PASS only if a reader unfamiliar with this session could
implement the benchmark from the spec alone, and no MEDIUM-or-higher findings."
phase: 2
```
~~~
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]