---
phase: 65-overridecontext-connection-teardown
plan: 06
type: execute
wave: 4
depends_on:
  - 65-03
  - 65-04
  - 65-05
files_modified:
  - src/lib.rs
  - src/parse.rs
  - cpp/src/shim.cpp
  - cpp/src/shim.hpp
  - tests/no_long_lived_conn.rs
  - Cargo.toml
  - test/integration/test_readonly_load.py
  - .planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md
autonomous: true
requirements:
  - LIFE-01
  - LIFE-02
  - LIFE-03
  - LIFE-04
tags:
  - duckdb
  - rust
  - ffi
  - lifecycle
  - close-out
  - structural-guard
  - watchdog
  - documentation

must_haves:
  truths:
    - "src/lib.rs:386-410 (H1 catalog_conn allocation + catalog_table_present probe + catalog_reader local) is DELETED"
    - "sv_register_parser_hooks signature on the C++ side drops both catalog_conn AND catalog_table_present args -- final signature is `sv_register_parser_hooks(duckdb_database db_handle, bool is_file_backed)` OR `sv_register_parser_hooks(duckdb_database db_handle)` if is_file_backed is also no-longer-needed (planner decision per RESEARCH §1.4 Step 2)"
    - "src/parse.rs::OverrideContext is fully retired -- either the struct is deleted along with SemanticViewsParserInfo's rust_state field, OR it slims to carry zero meaningful state (the CatalogReader field, the Drop impl with INTENTIONAL LEAK comment, the catalog_table_present field all go)"
    - "tests/no_long_lived_conn.rs exists with a syn-AST-scan test that fails if `init_extension` contains a call to `duckdb_connect` (success criterion 4)"
    - "syn is in Cargo.toml as a dev-dependency (if not transitively available)"
    - "test/integration/test_readonly_load.py has 4 new D-03b post-reopen tests: test_in_process_bootstrap_then_readonly_semantic_view_select, test_in_process_bootstrap_then_readonly_describe, test_in_process_bootstrap_then_readonly_show_dimensions, test_in_process_bootstrap_then_readonly_get_ddl"
    - "All 4 D-03b tests share the B1 bootstrap-then-RO prologue, wrap the RO reopen in _connect_with_watchdog at 5s, and assert the post-reopen call returns expected data within the watchdog budget"
    - "All 4 D-03b tests are appended to the run_test(...) call list in test/integration/test_readonly_load.py main()"
    - "test_in_process_bootstrap_then_readonly_fresh (B1), _existing (B2), test_in_process_load_only_then_readonly (B3), test_in_process_readonly_then_readwrite (B4), test_repeated_load_close_no_busy_spin (B11), and the 4 new D-03b tests ALL pass on this plan's final commit on milestone/v0.10.0"
    - "All 9 tests (B1-B4 + B11 + 4 D-03b) FAIL on v0.9.0 baseline (verified by running them on a v0.9.0 checkout before plan complete)"
    - ".planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md has the 'In-process RW→RO reopen of the same DB hangs (Phase 62 OverrideContext leak)' entry marked RESOLVED with a forward pointer to v0.10.0 / Phase 65 / the H1+H2 retirement commits (LIFE-04)"
    - "just test-all and just ci both exit 0 on milestone/v0.10.0"
    - "test/integration/test_adbc_transactions.py passes (D-21 invariant)"
    - "Phase 64 qualify_and_quote_table_ref wired sites at src/expand/sql_gen.rs:499,530,550 are NOT regressed (D-20 / D-22)"
  artifacts:
    - path: "tests/no_long_lived_conn.rs"
      provides: "Structural guard ensuring init_extension contains no duckdb_connect call"
      contains: "init_extension_has_no_duckdb_connect_call"
    - path: "test/integration/test_readonly_load.py"
      provides: "4 new D-03b post-reopen tests"
      contains: "test_in_process_bootstrap_then_readonly_semantic_view_select"
    - path: ".planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md"
      provides: "LIFE-04 closure ledger entry"
      contains: "RESOLVED"
  key_links:
    - from: "tests/no_long_lived_conn.rs"
      to: "src/lib.rs::init_extension"
      via: "syn AST walk asserting no duckdb_connect call"
      pattern: "init_extension_has_no_duckdb_connect_call"
    - from: "test/integration/test_readonly_load.py (4 D-03b tests)"
      to: "semantic_view / describe / SHOW / get_ddl on RO reopened conn"
      via: "_connect_with_watchdog + post-reopen call under watchdog"
      pattern: "_connect_with_watchdog.*read_only=True"
---

<objective>
Close out Phase 65. Retire H1 catalog_conn (the last long-lived extension-owned duckdb_connection handle), add the structural Rust unit test that fails CI if anyone re-introduces a long-lived native handle in init_extension (success criterion 4), extend test/integration/test_readonly_load.py with the 4 D-03b post-reopen tests that satisfy ROADMAP success criterion 3 in full, close the LIFE-04 ledger entry with a forward pointer, and verify just test-all + just ci green on milestone/v0.10.0.

The 5 watchdog tests from Plan 01 (B1-B4 + B11) plus the 4 new D-03b tests in this plan are the LIFE-01 / LIFE-02 / LIFE-03 / LIFE-04 acceptance evidence. They MUST fail on v0.9.0 baseline and MUST pass on this plan's final commit. Verifying both directions is part of the phase gate.

Purpose: This is the lifecycle close-out. After Plan 03 eliminated parser_override's catalog reads and Plan 05 retired H2 query_conn, H1 catalog_conn is the last remaining long-lived extension-owned handle in init_extension. Deleting it resolves the LIFE-01 in-process RW→RO hang root cause. The structural guard test prevents future regression. The D-03b post-reopen tests cover the read-side surface area (semantic_view SELECT, describe, SHOW DIMENSIONS, get_ddl) that Plan 01's B1/B2 only exercised via list_semantic_views.

Output: A milestone-ready milestone/v0.10.0 with both long-lived handles retired, structural protection in place, and the LIFE-01 watchdog evidence base extended to cover the full Phase 65 success criterion 3.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@CLAUDE.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/ROADMAP.md
@.planning/phases/65-overridecontext-connection-teardown/65-CONTEXT.md
@.planning/phases/65-overridecontext-connection-teardown/65-RESEARCH.md
@.planning/phases/65-overridecontext-connection-teardown/65-PATTERNS.md
@.planning/phases/65-overridecontext-connection-teardown/65-05-SUMMARY.md
@.planning/phases/65-overridecontext-connection-teardown/65-01-PLAN.md
@.planning/phases/65-overridecontext-connection-teardown/65-01-SUMMARY.md

<!-- Code touched -->
@src/lib.rs
@src/parse.rs
@cpp/src/shim.cpp
@cpp/src/shim.hpp
@Cargo.toml

<!-- Tests that must stay green / be extended -->
@test/integration/test_readonly_load.py
@test/integration/test_adbc_transactions.py

<!-- Ledger to update -->
@.planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md

<interfaces>
<!-- H1 retirement target (Task 1) -->

Delete src/lib.rs:386-410 entirely (the H1 catalog_conn allocation block + the catalog_table_present probe + the catalog_reader local):

```rust
let mut catalog_conn: ffi::duckdb_connection = ptr::null_mut();
let rc = unsafe { ffi::duckdb_connect(db_handle, &mut catalog_conn) };
if rc != ffi::DuckDBSuccess {
    return Err("Failed to create catalog connection".into());
}
// catalog_table_present probe -- delete entire block
let catalog_reader = crate::catalog::CatalogReader::new(catalog_conn, catalog_table_present);
```

After deletion, the sv_register_parser_hooks call shape becomes `sv_register_parser_hooks(db_handle, is_file_backed)` -- update both sides of the FFI to drop the catalog_conn + catalog_table_present args. If is_file_backed is also no-longer-needed (planner decides per RESEARCH §1.4 Step 2), drop it too.

<!-- OverrideContext final slim (Task 1 cont.) -->

src/parse.rs::OverrideContext (currently in v0.9.0 shape after Plan 03 revert):
  - Delete the `catalog: CatalogReader` field
  - Delete the Drop impl with INTENTIONAL LEAK comment
  - Delete the `catalog_table_present` field if it still exists
  - If only `is_file_backed` remains and no code consumes it, RETIRE THE STRUCT entirely + retire SemanticViewsParserInfo's rust_state field on the C++ side. The parser_override hook becomes ParserExtensionInfo-only with no Rust-side data.

Plan 03 left the struct in v0.9.0 shape transitionally; Plan 06's deletion is the close-out.

<!-- Structural guard test (Task 2) -->

```rust
// tests/no_long_lived_conn.rs
use syn::{visit::Visit, ItemFn, ExprCall};

#[test]
fn init_extension_has_no_duckdb_connect_call() {
    let src = std::fs::read_to_string("src/lib.rs").expect("read src/lib.rs");
    let file: syn::File = syn::parse_str(&src).expect("parse src/lib.rs");

    struct Finder { in_init_extension: bool, found: bool }
    impl<'ast> Visit<'ast> for Finder {
        fn visit_item_fn(&mut self, f: &'ast ItemFn) {
            let was = self.in_init_extension;
            if f.sig.ident == "init_extension" { self.in_init_extension = true; }
            syn::visit::visit_item_fn(self, f);
            self.in_init_extension = was;
        }
        fn visit_expr_call(&mut self, c: &'ast ExprCall) {
            if self.in_init_extension {
                if let syn::Expr::Path(p) = &*c.func {
                    if p.path.segments.last().map(|s| s.ident.to_string()).as_deref()
                        == Some("duckdb_connect") {
                        self.found = true;
                    }
                }
            }
            syn::visit::visit_expr_call(self, c);
        }
    }

    let mut f = Finder { in_init_extension: false, found: false };
    f.visit_file(&file);
    assert!(!f.found,
        "init_extension contains a duckdb_connect call site. \
         Phase 65 retired long-lived extension-owned duckdb_connection handles. \
         If a new connection is genuinely needed, open it via a per-call \
         Connection(*context.db) inside a bind/exec callback instead.");
}
```

<!-- 4 D-03b post-reopen tests (Task 3) -->

Each shares the B1 prologue from test_in_process_bootstrap_then_readonly_fresh (test/integration/test_readonly_load.py lines 425-461) and differs only in the post-reopen call. Sketches:

```python
def test_in_process_bootstrap_then_readonly_semantic_view_select():
    """D-03b #1: post-reopen semantic_view() SELECT (LIFE-01 / criterion 3)."""
    with tempfile.TemporaryDirectory() as tmp:
        db = str(Path(tmp) / "sv_select.duckdb")
        w = open_writable(db)
        w.execute("CREATE TABLE t (i INT, j INT)")
        w.execute("INSERT INTO t VALUES (1,10),(2,20)")
        w.execute("CREATE SEMANTIC VIEW v TABLES (t PRIMARY KEY (i)) DIMENSIONS (i AS t.i) METRICS (s AS SUM(j))")
        w.close(); del w; gc.collect()

        ro, elapsed = _connect_with_watchdog(db, watchdog_seconds=5.0, read_only=True, config=_connect_config())
        try:
            assert elapsed < 5.0
            ro.execute("LOAD semantic_views")
            rows = ro.execute(
                "SELECT * FROM semantic_view('v', dimensions := ['i'], metrics := ['s']) ORDER BY i"
            ).fetchall()
            assert rows == [(1, 10), (2, 20)], rows
        finally:
            ro.close()

def test_in_process_bootstrap_then_readonly_describe():
    """D-03b #2: post-reopen describe_semantic_view()."""
    # Same prologue + RO reopen
    rows = ro.execute("SELECT * FROM describe_semantic_view('v')").fetchall()
    assert len(rows) > 0

def test_in_process_bootstrap_then_readonly_show_dimensions():
    """D-03b #3: post-reopen SHOW SEMANTIC DIMENSIONS (representative SHOW)."""
    rows = ro.execute("SHOW SEMANTIC DIMENSIONS FROM v").fetchall()
    assert any('i' in str(r) for r in rows)

def test_in_process_bootstrap_then_readonly_get_ddl():
    """D-03b #4: post-reopen get_ddl() round-trip."""
    ddl = ro.execute("SELECT get_ddl('v')").fetchone()[0]
    assert "CREATE OR REPLACE SEMANTIC VIEW" in ddl
    assert "v" in ddl
```

<!-- deferred-items.md closure entry (Task 4) -->

The entry to update (in .planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md):
"In-process RW→RO reopen of the same DB hangs (Phase 62 OverrideContext leak)"

Add a "Status: RESOLVED" line + forward pointer:
"Resolved in v0.10.0 Phase 65 via H1 catalog_conn retirement (Plan 06) + H2 query_conn retirement (Plan 05) + parser_override slimming to zero catalog reads (Plan 03). See .planning/phases/65-overridecontext-connection-teardown/ for the full record."
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1 (Wave 1): Retire H1 catalog_conn and finalize OverrideContext slim</name>
  <files>src/lib.rs, src/parse.rs, cpp/src/shim.cpp, cpp/src/shim.hpp, src/catalog.rs</files>
  <read_first>
    - 65-CONTEXT.md `<decisions>` D-18 (Plan 06 retires H1 + structural guard test), `<specifics>` Plan 03 end-state H1 still allocated bullet
    - 65-RESEARCH.md §1.4 (Plan 06 Tasks 1-2) + §9 (H1+H2 retirement map)
    - 65-PATTERNS.md "src/lib.rs:386-410 H1 retirement" (lines 524-535), "src/parse.rs::OverrideContext final slim" (lines 539-541)
    - src/lib.rs (current state, especially lines 386-410 H1 allocation, line 420 sv_register_parser_hooks call, the surrounding init_extension code)
    - src/parse.rs (current state of OverrideContext per Plan 03 revert -- v0.9.0 shape with CatalogReader field + Drop impl with INTENTIONAL LEAK comment; sv_make_override_context FFI entry; the 4 rewrite_* sites that may still reference OverrideContext.catalog)
    - cpp/src/shim.cpp (current sv_register_parser_hooks at the post-Plan-03 v0.9.0 signature + SemanticViewsParserInfo destructor with INTENTIONAL LEAK comment)
    - cpp/src/shim.hpp (current declaration)
    - src/catalog.rs (CatalogReader struct -- should be a stack-constructed-per-call type after Plan 05; verify no long-lived consumer remains)
  </read_first>
  <behavior>
    - Test 1 (compile gate): `cargo check --features extension --no-default-features` exits 0 after the H1 deletion + OverrideContext slim + sv_register_parser_hooks signature change.
    - Test 2 (link gate): `just build` exits 0; FFI signatures agree between Rust and C++.
    - Test 3 (structural): grep `let mut catalog_conn` src/lib.rs returns 0 matches.
    - Test 4 (structural): grep `duckdb_connect\(` src/lib.rs returns 0 matches inside init_extension (the structural guard in Task 2 enforces this).
    - Test 5 (structural): grep `INTENTIONAL LEAK` src/parse.rs and cpp/src/shim.cpp both return 0 matches (the leak rationale is moot once H1 retires).
    - Test 6: `just test-sql`, `cargo test`, and `uv run test/integration/test_adbc_transactions.py` all pass -- no functional regression from H1 removal.
    - Test 7: A specific test that previously consumed H1 indirectly (any parser_override path) continues to work because Plans 03/04 eliminated the catalog reads inside parser_override. Verify via test/sql/65_pk_error.test, test/sql/65_metadata_via_sql.test, test/sql/65_alter_comment_merge_patch.test, test/integration/test_create_from_yaml_v010.py.
  </behavior>
  <action>
**Step A -- Delete H1 catalog_conn allocation + catalog_reader + catalog_table_present probe in src/lib.rs:**

In src/lib.rs::init_extension, delete the block at lines 386-410 entirely:
  - The `let mut catalog_conn: ffi::duckdb_connection = ptr::null_mut();`
  - The `duckdb_connect(db_handle, &mut catalog_conn)` call + error check
  - The `catalog_table_present` probe (querying information_schema.tables for semantic_layer._definitions)
  - The `let catalog_reader = CatalogReader::new(catalog_conn, catalog_table_present);` construction

If catalog_reader is referenced anywhere downstream (e.g. for the slim SemanticViewsParserInfo or for the parser-hook setup), audit those references and refactor.

**Step B -- Slim sv_register_parser_hooks signature on both sides:**

The current (Plan 03 reverted) signature is `(duckdb_connection catalog_conn, bool is_file_backed)` per `<interfaces>` in 65-03-PLAN.md. Plan 06's target is the smallest possible signature.

Decide:
  - Does sv_register_parser_hooks still need is_file_backed? Per RESEARCH §1.4 the flag previously gated whether CREATE-time type inference ran; Plan 03 already removed that gate. The bind callbacks under Plan 05 each compute is_file_backed locally from `Connection probe(*context.db).is_file_backed()` or equivalent. Therefore is_file_backed is also dead.
  - If both args are dead, final signature: `sv_register_parser_hooks(duckdb_database db_handle)` -- the parser_override hook may still need to know the db_handle for ParserExtensionInfo construction.

Update cpp/src/shim.cpp + shim.hpp + src/lib.rs to use the minimum-arg signature. Verify build.

**Step C -- Retire OverrideContext (or slim to zero state):**

In src/parse.rs::OverrideContext (currently the v0.9.0 shape with `catalog: CatalogReader, is_file_backed: bool` per Plan 03):
  - Delete the `catalog: CatalogReader` field
  - Delete the `Drop` impl with INTENTIONAL LEAK comment
  - Delete the `is_file_backed` field if not consumed anywhere
  - If both fields are gone and no consumer remains, DELETE the struct entirely. Then delete SemanticViewsParserInfo's `rust_state` field on the C++ side; the parser_override hook works with just ParserExtensionInfo per RESEARCH §1.4 Step 2.

Audit src/parse.rs's 4 rewrite_* sites (rewrite_drop, rewrite_alter_rename, rewrite_alter_comment, rewrite_yaml_file_create) for any remaining reference to `ctx.catalog` -- by Plan 05's end, all read-paths went per-call, and Plan 04's end migrated rewrite_yaml_file_create + rewrite_alter_comment off catalog access. rewrite_drop and rewrite_alter_rename use the race-guard pattern (D-13) which doesn't need catalog access. Therefore all 4 sites should be `ctx.catalog`-free at this point.

In cpp/src/shim.cpp:
  - Delete the INTENTIONAL LEAK comment from `~SemanticViewsParserInfo` (lines 159-181 region).
  - If SemanticViewsParserInfo's rust_state can be retired, delete it.

**Step D -- Verify CatalogReader is now per-call only:**

In src/catalog.rs, verify that CatalogReader::new accepts a per-call duckdb_connection (Plan 05 should have made this change). If any long-lived consumer remains, refactor or delete. The struct can stay as a helper -- it just no longer owns a long-lived handle.

**Step E -- Build + smoke test:**

Run `cargo check`, `just build`, `cargo test --lib --features extension --no-default-features`, `just test-sql`, `uv run test/integration/test_adbc_transactions.py`. All must pass.

Commit. This is the H1 retirement commit referenced by LIFE-04.
  </action>
  <verify>
    <automated>cargo check --features extension --no-default-features 2>&amp;1 | tail -10 ; just build 2>&amp;1 | tail -10 ; cargo test --lib --features extension --no-default-features 2>&amp;1 | tail -20 ; just test-sql 2>&amp;1 | tail -20 ; uv run test/integration/test_adbc_transactions.py 2>&amp;1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `cargo check --features extension --no-default-features` exits 0
    - `just build` exits 0
    - `just test-sql` exits 0
    - `uv run test/integration/test_adbc_transactions.py` exits 0 (D-21 invariant)
    - `grep -nE 'let mut catalog_conn' src/lib.rs` returns 0 matches
    - `grep -nE 'duckdb_connect\(' src/lib.rs` returns 0 matches inside `init_extension` (use line-range grep around the init_extension function body)
    - `grep -nE 'catalog_table_present' src/lib.rs` returns 0 matches
    - `grep -c 'INTENTIONAL LEAK' src/parse.rs cpp/src/shim.cpp` returns 0 from both files
    - `grep -nE 'pub struct OverrideContext' src/parse.rs` returns 0 matches OR returns a struct with no `catalog` field (planner's choice on full retire vs slim-to-empty)
    - sv_register_parser_hooks final signature is `(duckdb_database db_handle)` OR `(duckdb_database db_handle, bool is_file_backed)` (planner decision documented in SUMMARY)
  </acceptance_criteria>
  <done>
H1 catalog_conn is retired. OverrideContext is either deleted or slimmed to empty. sv_register_parser_hooks signature is minimal. INTENTIONAL LEAK rationale is gone from the codebase. No long-lived extension-owned duckdb_connection exists in init_extension. Task 2's structural guard test now has a real target to assert against.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2 (Wave 2): Add the structural guard test (tests/no_long_lived_conn.rs)</name>
  <files>tests/no_long_lived_conn.rs, Cargo.toml</files>
  <read_first>
    - 65-CONTEXT.md `<decisions>` D-18 Plan 06 bullet (structural Rust unit test); ROADMAP success criterion 4
    - 65-RESEARCH.md §7 (entire section -- approaches surveyed, Option A recommended, sketch at lines 572-609)
    - 65-PATTERNS.md "tests/no_long_lived_conn.rs (new structural guard)" section (lines 543-582)
    - src/lib.rs current state after Task 1 (the init_extension function -- the structural guard parses this file)
    - tests/output_proptest.rs (existing standalone integration test under tests/ -- template for the file location and crate-test integration)
    - Cargo.toml (current dev-dependencies; verify whether `syn` is already available transitively via duckdb-rs or proc-macro deps; if not, add as `[dev-dependencies] syn = "2"`)
  </read_first>
  <behavior>
    - Test 1 (positive): `cargo test --test no_long_lived_conn` exits 0 after Task 1's H1 retirement (no duckdb_connect call in init_extension).
    - Test 2 (negative -- regression sentinel): If a developer re-introduces `duckdb_connect(...)` inside init_extension, the test FAILS with the error message from the assert. Verify by temporarily adding a `let mut x = ptr::null_mut(); duckdb_connect(db_handle, &mut x);` inside init_extension, running `cargo test --test no_long_lived_conn`, observing the failure, then reverting the temporary change.
    - Test 3 (positive -- false-positive avoidance): If a `duckdb_connect` call exists OUTSIDE init_extension (e.g. inside a bind callback or test code), the structural guard does NOT flag it. The Finder visitor scopes `in_init_extension` correctly per RESEARCH §7.2 lines 588-595.
    - Test 4 (positive -- alias resolution): If someone re-introduces a connection via an aliased path (e.g. `use ffi::duckdb_connect as my_connect; my_connect(...)`), the guard catches it OR explicitly documents the limitation. Implementation: the visitor walks the last path segment of the call's function expression -- aliasing via `use ... as` evades it. Document the limitation in a code comment. Per D-22 bounded scope, simple grep-via-syn is acceptable; a fully reliable scan would require resolving the use-graph, out of Plan 06 scope.
  </behavior>
  <action>
**Step A -- Verify syn availability:**

Check Cargo.toml for syn dev-dep. Run `cargo tree -i syn` to confirm whether it's available transitively. If not, add to Cargo.toml under [dev-dependencies]:

```toml
[dev-dependencies]
# ... existing entries ...
syn = { version = "2", features = ["full", "visit"] }
```

The `visit` feature is required for the `syn::visit::Visit` trait per RESEARCH §7.2.

**Step B -- Create tests/no_long_lived_conn.rs:**

Use the verbatim sketch from RESEARCH §7.2 lines 572-609 (also reproduced in `<interfaces>` block of this PLAN.md). Make the assert message explicit so a future developer immediately understands the failure:

> `init_extension contains a duckdb_connect call site. Phase 65 (v0.10.0) retired long-lived extension-owned duckdb_connection handles. If a new connection is genuinely needed, open it via a per-call Connection(*context.db) inside a bind/exec callback instead. See .planning/phases/65-overridecontext-connection-teardown/ for context.`

Add a module-level comment explaining the test's purpose, the success-criterion-4 anchor, and the known limitation (use-as aliases are not detected).

**Step C -- Run the test on the current (post-Task-1) tree:**

`cargo test --test no_long_lived_conn` -- expect PASS (no duckdb_connect in init_extension after Task 1's deletion).

**Step D -- Negative regression check:**

Manually verify the test catches a regression. Temporarily add inside `init_extension`:

```rust
let mut _x: ffi::duckdb_connection = ptr::null_mut();
let _rc = unsafe { ffi::duckdb_connect(db_handle, &mut _x) };
```

Re-run `cargo test --test no_long_lived_conn` and observe FAILURE with the expected assert message. Then REVERT the change (do NOT commit it). This validates the guard works.

**Step E -- Integrate into CI:**

Verify `just ci` (which runs `cargo test --all` or equivalent) picks up the new test. If `just ci` does not currently invoke the standalone integration tests under `tests/`, audit the justfile and add `cargo test --test no_long_lived_conn` to the pre-push check list if needed.
  </action>
  <verify>
    <automated>cargo test --test no_long_lived_conn 2>&amp;1 | tail -10 ; cargo tree -i syn 2>&amp;1 | head -5</automated>
  </verify>
  <acceptance_criteria>
    - `test -f tests/no_long_lived_conn.rs` returns 0
    - `cargo test --test no_long_lived_conn` exits 0
    - `cargo tree -i syn 2>&1 | wc -l` returns > 0 (syn dependency satisfied either transitively or as new dev-dep)
    - The negative regression check (Step D) was demonstrated to fail on a temporary regression and pass after revert (verify in 65-06-SUMMARY.md by including the lldb / test output snapshot)
    - Module-level comment in tests/no_long_lived_conn.rs documents the use-as alias limitation
    - The assert message includes the explicit Phase 65 / v0.10.0 + per-call Connection guidance
  </acceptance_criteria>
  <done>
The structural guard test is live. Any future re-introduction of a long-lived duckdb_connection in init_extension fails the CI gate.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3 (Wave 2): Extend test_readonly_load.py with 4 D-03b post-reopen tests</name>
  <files>test/integration/test_readonly_load.py</files>
  <read_first>
    - 65-CONTEXT.md `<decisions>` D-03b (the 4 specific test scenarios + acceptance shape); ROADMAP success criterion 3
    - 65-RESEARCH.md §8 (sketches for all 4 tests, lines 614-661)
    - 65-PATTERNS.md "4 D-03b post-reopen tests" section (lines 584-624) including the verbatim B1 prologue at lines 588-605
    - test/integration/test_readonly_load.py (entire file -- especially the B1 test test_in_process_bootstrap_then_readonly_fresh at lines 425-461, the helpers open_writable, _connect_with_watchdog, _connect_config, _minimal_create_sql, and the main() run_test registration site around lines 645-649)
    - The Task 6 migration outcome in Plan 05: semantic_view, describe_semantic_view, get_ddl, SHOW SEMANTIC DIMENSIONS should all work post-reopen on RO because their bind callbacks open per-call Connection(*context.db)
  </read_first>
  <behavior>
    - Test 1 (D-03b #1): `test_in_process_bootstrap_then_readonly_semantic_view_select` passes -- post-reopen `SELECT * FROM semantic_view('v', ...)` returns expected rows within watchdog budget.
    - Test 2 (D-03b #2): `test_in_process_bootstrap_then_readonly_describe` passes -- post-reopen `describe_semantic_view('v')` returns rows.
    - Test 3 (D-03b #3): `test_in_process_bootstrap_then_readonly_show_dimensions` passes -- post-reopen `SHOW SEMANTIC DIMENSIONS FROM v` returns the expected dimension(s).
    - Test 4 (D-03b #4): `test_in_process_bootstrap_then_readonly_get_ddl` passes -- post-reopen `SELECT get_ddl('v')` returns a DDL string containing the view name and `CREATE OR REPLACE SEMANTIC VIEW`.
    - Test 5 (regression): The original B1-B4 + B11 tests from Plan 01 (5 tests) continue to pass.
    - Test 6 (baseline regression -- documented, not auto-tested): On a fresh v0.9.0 checkout, the 4 new D-03b tests + B1-B4 + B11 all FAIL (verifies the test surface is a real fail-on-baseline / pass-on-fix gate). Document the baseline-failure evidence in 65-06-SUMMARY.md by running on a temporary v0.9.0 worktree.
  </behavior>
  <action>
**Step A -- Add the 4 test functions to test/integration/test_readonly_load.py:**

Use the prologue from B1 (lines 425-461) verbatim. The 4 tests differ only in the post-reopen call and the assertion. Add them adjacent to B1 in the file for cohesion.

For each test:
  1. Setup tempdir + db path.
  2. `w = open_writable(db)` -- bootstrap connection.
  3. CREATE TABLE t with at least one column suitable for the SELECT test (`CREATE TABLE t (i INT, j INT); INSERT INTO t VALUES (1, 10), (2, 20);` for the semantic_view test; smaller setup works for the others).
  4. `w.execute("CREATE SEMANTIC VIEW v TABLES (t PRIMARY KEY (i)) DIMENSIONS (i AS t.i) METRICS (s AS SUM(j))")` -- include enough so describe / SHOW DIMENSIONS / get_ddl have meaningful content.
  5. `w.close(); del w; gc.collect()` -- ensure writer connection is fully released before the RO reopen attempt.
  6. `ro, elapsed = _connect_with_watchdog(db, watchdog_seconds=5.0, read_only=True, config=_connect_config())` -- the watchdog-wrapped reopen.
  7. `assert elapsed < 5.0` -- the reopen must succeed within budget.
  8. `ro.execute("LOAD semantic_views")` -- load the extension on the RO connection.
  9. The per-test post-reopen call + assertion (per `<interfaces>` block above).
  10. `finally: ro.close()`.

**Step B -- Register the 4 new tests in main()'s run_test call list:**

The current main() function lists each test by name (around lines 645-649 per PATTERNS.md line 624). Append the 4 new functions to that list so they actually execute when the file runs.

**Step C -- Run on the current (post-Task-2) tree:**

`uv run test/integration/test_readonly_load.py` -- expect ALL 9 tests pass (B1-B4 + B11 + 4 D-03b).

**Step D -- Verify fail-on-baseline:**

Per ROADMAP success criterion 3 "fails on v0.9.0 baseline and passes on v0.10.0", verify the new D-03b tests genuinely require Phase 65's fixes. Use a temporary worktree on the v0.9.0 tag (`git worktree add /tmp/v090 v0.9.0`) and run the 4 new tests there. Expected outcome: all 4 fail (either via watchdog timeout at the reopen or via the post-reopen call hanging / erroring). Document the evidence in 65-06-SUMMARY.md.

After verification, remove the temporary worktree (`git worktree remove /tmp/v090`).

**Step E -- Re-run on milestone/v0.10.0:**

`just test-all` and `uv run test/integration/test_readonly_load.py` -- expect all green.
  </action>
  <verify>
    <automated>uv run test/integration/test_readonly_load.py 2>&amp;1 | tail -30 ; just test-all 2>&amp;1 | tail -15</automated>
  </verify>
  <acceptance_criteria>
    - `uv run test/integration/test_readonly_load.py` exits 0 with all 9 tests passing (5 from Plan 01 + 4 D-03b new)
    - `just test-all` exits 0
    - The 4 new test functions exist with the names from `<interfaces>` (verifiable by grep): test_in_process_bootstrap_then_readonly_semantic_view_select, test_in_process_bootstrap_then_readonly_describe, test_in_process_bootstrap_then_readonly_show_dimensions, test_in_process_bootstrap_then_readonly_get_ddl
    - Each test wraps the RO reopen in `_connect_with_watchdog(db, watchdog_seconds=5.0, read_only=True, config=_connect_config())` and asserts `elapsed < 5.0`
    - Each test exercises a different read-side bind callback (Plan 05's migrations -- semantic_view, describe_semantic_view, show_semantic_dimensions, get_ddl) on the RO reopened connection
    - main()'s run_test list contains all 4 new tests
    - The baseline-fail evidence (Step D) is captured in 65-06-SUMMARY.md (paste of the v0.9.0 worktree run output)
  </acceptance_criteria>
  <done>
The Phase 65 success-criterion 3 acceptance evidence is complete. Plan 01's watchdog suite is extended from "list_semantic_views post-reopen" to "all four major read paths post-reopen". The lifecycle fix is observable from the user's perspective for every category of read-side operation.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 4 (Wave 2): Close LIFE-04 ledger entry + final just ci gate</name>
  <files>.planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md</files>
  <read_first>
    - 65-CONTEXT.md `<decisions>` D-18 Plan 06 bullet (LIFE-04 ledger close); LIFE-04 in REQUIREMENTS.md
    - .planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md (existing entry around line 62+ -- "In-process RW→RO reopen of the same DB hangs (Phase 62 OverrideContext leak)")
    - 65-PATTERNS.md "LIFE-04 ledger close" section (lines 626-628)
  </read_first>
  <behavior>
    - Test 1: deferred-items.md has a "Status: RESOLVED" line on the relevant entry.
    - Test 2: deferred-items.md has a forward pointer to v0.10.0 / Phase 65 / specific commit SHAs (or to the phase directory).
    - Test 3: `just test-all` and `just ci` both exit 0 on milestone/v0.10.0.
  </behavior>
  <action>
**Step A -- Update deferred-items.md:**

Open .planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md. Find the entry "In-process RW→RO reopen of the same DB hangs (Phase 62 OverrideContext leak)" (around line 62+ per PATTERNS.md).

Add (preserving the existing entry content):

```
**Status:** RESOLVED in v0.10.0 (2026-05-XX).

**Resolution:** Phase 65 retired both long-lived extension-owned `duckdb_connection` handles:
- H1 catalog_conn (`src/lib.rs:386-410`) — retired in Plan 06.
- H2 query_conn (`src/lib.rs:498-507`) — retired in Plan 05.

The fix path eliminated catalog reads inside `parser_override` (Plan 03) rather than relocating them, then migrated all 17 read-side callbacks (Plan 05) to the C++ Catalog API shim where each bind callback opens a per-call `Connection(*context.db)`. Structural Rust unit test `tests/no_long_lived_conn.rs` (Plan 06 Task 2) prevents re-introduction.

**Forward pointer:** `.planning/phases/65-overridecontext-connection-teardown/`
- 65-CONTEXT.md (locked architecture)
- 65-RESEARCH.md (implementation map)
- 65-03-SUMMARY.md through 65-06-SUMMARY.md (per-plan delivery records)

**Watchdog evidence:** `test/integration/test_readonly_load.py` tests B1-B4 + B11 (Plan 01) + 4 D-03b post-reopen tests (Plan 06 Task 3) all flip from FAIL on v0.9.0 to PASS on v0.10.0.
```

Replace `2026-05-XX` with today's date.

**Step B -- Final phase gate:**

Run `just test-all` and `just ci` on milestone/v0.10.0. Both must exit 0. If either fails, reconcile before declaring the task done.

If a failure appears that is unrelated to Phase 65's scope, surface it as TECH-DEBT or a Phase 66 follow-up per D-22 (do NOT silently absorb).

**Step C -- Document any cross-database EXPAND-CTX investigation outcome:**

Per Plan 05's Task 6 instructions, the question of whether the EXPAND-CTX-01 root cause dissolved after H2 retirement may have surfaced findings. Forward those findings into 65-06-SUMMARY.md (or, if relevant, into a v0.10.0 Phase 66 prep note).

**Step D -- Verify Phase 64 wiring preserved (D-20):**

Run the Phase 64 sqllogictest suite (anything matching `test/sql/64_*` or `test/sql/phase64_*`) and confirm all pass byte-identical. The qualify_and_quote_table_ref call sites at src/expand/sql_gen.rs:499,530,550 should be unchanged.
  </action>
  <verify>
    <automated>grep -A 10 "In-process RW.*RO reopen" .planning/milestones/v0.9.0-phases/63-readonly-database-load-support/deferred-items.md 2>&amp;1 | head -20 ; just test-all 2>&amp;1 | tail -15 ; just ci 2>&amp;1 | tail -15 ; grep "qualify_and_quote_table_ref" src/expand/sql_gen.rs | wc -l</automated>
  </verify>
  <acceptance_criteria>
    - The deferred-items.md entry has "Status: RESOLVED" + forward pointer to .planning/phases/65-overridecontext-connection-teardown/
    - The entry references at least one commit SHA (the H1 retirement commit from Task 1 OR the H2 retirement commit from Plan 05 Task 6)
    - `just test-all` exits 0
    - `just ci` exits 0
    - `grep -c qualify_and_quote_table_ref src/expand/sql_gen.rs` returns the same count as before Plan 05 (verify the Phase 64 wiring is preserved)
    - test/integration/test_adbc_transactions.py passes (D-21)
    - Any TECH-DEBT entries or Phase 66 follow-ups surfaced during Plan 05-06 are documented in either 65-06-SUMMARY.md or .planning/TECH-DEBT.md (per D-22 bounded scope + signal surfacing)
  </acceptance_criteria>
  <done>
Phase 65 is complete. LIFE-01 / LIFE-02 / LIFE-03 / LIFE-04 are all satisfied. The watchdog evidence base is full. The structural guard prevents future regression. The milestone is ready for Phase 66 (release-prep + ADBC tests + CHANGELOG).
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| C-API FFI ↔ Rust at extension load (sv_register_parser_hooks final signature) | Crossed once per extension load; ABI mismatch causes link/runtime failure |
| init_extension code path | Now contains no duckdb_connect call -- the structural guard protects future regressions |
| Per-call Connection inside bind callbacks (inherited from Plan 05) | Continues to be the load-bearing primitive for all 17 read paths |
| Watchdog tests' subprocess thread | Bootstrap thread + RO reopen thread + watchdog timer; race conditions if any pattern leaks long-lived state |
| Documentation ledger update | Low-stakes; pure text edit |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-65-06-01 | Tampering | sv_register_parser_hooks final ABI | mitigate | Both Rust (src/lib.rs caller + src/parse.rs::sv_make_override_context signature) and C++ (cpp/src/shim.cpp + shim.hpp) must agree on the minimum-arg signature. Build gate at Task 1 (`cargo check` + `just build`) catches mismatch. |
| T-65-06-02 | Tampering | Structural guard test bypass via `use ... as` alias | accept | Documented limitation. Per D-22, a fully-reliable scan would require resolving the use-graph, which is out of Plan 06 scope. Simple grep-via-syn catches the common case (`duckdb_connect(...)` calls inside init_extension). |
| T-65-06-03 | Denial of Service | Watchdog test thread leak on baseline | accept | Existing documented behavior from Plan 01 -- when the RO reopen hangs on v0.9.0, the bootstrap thread stays alive after the watchdog timer fires. Acceptable because (a) the watchdog test's job is to surface the hang as a test failure, not to clean up the leaked thread; (b) the test process exits after, cleaning up everything. Documented in Plan 01 SUMMARY. |
| T-65-06-04 | Information Disclosure | deferred-items.md commit SHA reference | accept | Public ledger entries reference public commits. No sensitive data exposed. |
| T-65-06-05 | Tampering | Phase 64 qualify_and_quote_table_ref accidental regression | mitigate | D-20 + D-22 invariants. Task 4 explicit `grep -c qualify_and_quote_table_ref src/expand/sql_gen.rs` and Phase 64 sqllogictest run gate. |
| T-65-06-06 | Repudiation / Audit | LIFE-04 closure must be discoverable | mitigate | The ledger update includes forward pointer to .planning/phases/65-overridecontext-connection-teardown/ and at least one commit SHA, ensuring future investigators can trace the resolution path. |
| T-65-06-SC | Tampering | npm/pip/cargo installs (syn dev-dep) | mitigate | If `syn` isn't already transitively available, it's added as a [dev-dependencies] entry. `syn` is a foundational Rust ecosystem crate (millions of downloads, maintained by dtolnay) -- legitimacy is well-established. No package-legitimacy gate checkpoint needed -- this is not [ASSUMED] or [SUS]. |
</threat_model>

<verification>
- `cargo check --features extension --no-default-features` exits 0 after Task 1
- `just build` exits 0 after Task 1
- `cargo test --test no_long_lived_conn` exits 0 after Task 2
- `uv run test/integration/test_readonly_load.py` exits 0 with all 9 tests passing (B1-B4 + B11 + 4 D-03b) after Task 3
- `just test-all` exits 0 after Task 4
- `just ci` exits 0 after Task 4
- `uv run test/integration/test_adbc_transactions.py` exits 0 (D-21 invariant)
- `grep -nE 'let mut catalog_conn|let mut query_conn' src/lib.rs` returns 0 matches
- `grep -nE 'duckdb_connect\(' src/lib.rs` returns 0 matches inside `init_extension`
- `grep -c 'INTENTIONAL LEAK' src/parse.rs cpp/src/shim.cpp` returns 0 from both
- The deferred-items.md entry has "Status: RESOLVED" and a forward pointer
- The 4 D-03b tests fail on v0.9.0 baseline (verified via temporary worktree per Task 3 Step D; evidence pasted into 65-06-SUMMARY.md)
- Phase 64 qualify_and_quote_table_ref call sites unchanged (D-20)
</verification>

<success_criteria>
- H1 catalog_conn retired (LIFE-02 / success criterion 4 first half)
- OverrideContext slimmed to empty or fully retired
- Structural guard test (tests/no_long_lived_conn.rs) fails CI if anyone re-introduces a long-lived duckdb_connection in init_extension (success criterion 4 second half)
- 4 D-03b post-reopen tests pass on milestone/v0.10.0 (success criterion 3)
- Plan 01's B1-B4 + B11 tests pass on milestone/v0.10.0 (LIFE-01)
- Both the 5 Plan 01 tests AND the 4 D-03b tests FAIL on v0.9.0 baseline (criterion 3 fails-on-baseline gate)
- deferred-items.md entry closed with forward pointer (LIFE-04)
- D-21 transactional invariant green throughout (ADBC tests)
- D-20 invariant green (Phase 64 qualify wiring preserved)
- just test-all and just ci both green on milestone/v0.10.0
- Phase 65 fully delivers LIFE-01 / LIFE-02 / LIFE-03 / LIFE-04
- Phase 66 unblocked (release-prep + ADBC tests + CHANGELOG entry)
</success_criteria>

<output>
Create `.planning/phases/65-overridecontext-connection-teardown/65-06-SUMMARY.md` documenting:
- Task 1: H1 retirement diff stat + final sv_register_parser_hooks signature decision (whether is_file_backed survives) + OverrideContext final disposition (retired vs slimmed-to-empty)
- Task 2: structural guard test pass output + negative regression check evidence (the temporary regression's failure output, then revert confirmation)
- Task 3: all 9 tests pass on milestone/v0.10.0 output; baseline-fail evidence (run output from v0.9.0 worktree on the 4 new tests)
- Task 4: deferred-items.md before/after diff; `just test-all` + `just ci` green confirmation
- Cross-database EXPAND-CTX investigation outcome (forwarded from Plan 05 Task 6) -- either "root cause dissolved post-H2 retirement, Phase 66 EXPAND-CTX-01 is a no-op" OR "specific failure surfaced, filed as Phase 66 follow-up X"
- TECH-DEBT entries or Phase 66 follow-ups surfaced during the phase (D-22 bounded scope + signal surfacing)
- Phase 65 close-out statement: LIFE-01, LIFE-02, LIFE-03, LIFE-04 satisfied. Forward pointer to Phase 66 for release prep.
</output>
