---
phase: 65-overridecontext-connection-teardown
plan: 05
type: execute
wave: 3
depends_on:
  - 65-03
  - 65-04
files_modified:
  - cpp/src/shim.cpp
  - cpp/src/shim.hpp
  - src/lib.rs
  - src/ddl/list.rs
  - src/ddl/describe.rs
  - src/ddl/show_columns.rs
  - src/ddl/show_dims.rs
  - src/ddl/show_dims_for_metric.rs
  - src/ddl/show_metrics.rs
  - src/ddl/show_facts.rs
  - src/ddl/show_materializations.rs
  - src/ddl/get_ddl.rs
  - src/ddl/read_yaml.rs
  - src/query/table_function.rs
  - src/query/explain.rs
  - src/catalog.rs
  - src/type_cache.rs
  - test/sql/65_read_bridge_spike.test
  - test/sql/TEST_LIST
  - test/integration/test_concurrent_reads_per_call_conn.py
autonomous: true
requirements:
  - LIFE-01
  - LIFE-02
tags:
  - duckdb
  - rust
  - ffi
  - cpp
  - catalog-api
  - read-path
  - bind-callback
  - per-call-connection
  - type-inference

must_haves:
  truths:
    - "Wave 0 sqllogictest spike (65_read_bridge_spike.test) confirms Rust callbacks are reachable from C++ bind callbacks via sv_register_table_function and that Connection(*context.db) works from the bridged Rust side (A3 resolution)"
    - "All 17 read-side registrations (15 table functions + 2 scalars per RESEARCH §3) migrate from duckdb-rs `register_table_function_with_extra_info` / `register_scalar_function_with_state` to the C++ Catalog API shim sv_register_table_function (and a new sibling sv_register_scalar_function for the 2 scalars)"
    - "Each migrated bind callback opens a per-call `Connection(*context.db)` for catalog reads; no read-side code path consumes a long-lived extension-owned duckdb_connection after migration"
    - "src/lib.rs:498-507 (H2 query_conn allocation block) is DELETED in the final commit of this plan"
    - "QueryState struct (src/query/table_function.rs:33-37) is retired (or refactored to take a per-call conn at method-call time)"
    - "CatalogReader stays as a struct but its constructor accepts a per-call duckdb_connection; long-lived ownership of catalog_conn ends"
    - "Process-local type-inference cache lives in src/type_cache.rs as `OnceLock<RwLock<HashMap<(String, u64), Arc<InferredTypes>>>>` keyed on (view_name, schema_fingerprint); RESEARCH §6.2; explicitly NOT a bounded LRU (TECH-DEBT 20 anti-pattern)"
    - "Type inference (LIMIT 0 probe via try_infer_schema) runs at read-side bind time on a per-call connection; D-16 / D-17 -- never persisted to _definitions"
    - "Phase 64 qualify_and_quote_table_ref call sites at src/expand/sql_gen.rs:499,530,550 are NOT regressed by this plan (D-20)"
    - "test/integration/test_adbc_transactions.py passes throughout the 6-wave migration (D-21 invariant)"
    - "test/integration/test_multi_db_isolation.py (existing) passes after H2 retirement -- semantic_view() against an attached DB returns rows on the caller's catalog/schema search path"
    - "test/integration/test_concurrent_reads_per_call_conn.py passes -- 8 parallel SHOW SEMANTIC DIMENSIONS calls each open their own per-call connection without contention"
    - "After H2 retirement, `just test-sql`, `cargo test`, and `just ci` are all green on milestone/v0.10.0"
  artifacts:
    - path: "cpp/src/shim.cpp"
      provides: "Extended sv_register_table_function + new sv_register_scalar_function"
      contains: "sv_register_scalar_function"
    - path: "src/type_cache.rs"
      provides: "Process-local unbounded HashMap cache for inferred column types"
      contains: "OnceLock"
    - path: "test/sql/65_read_bridge_spike.test"
      provides: "Wave 0 bridge spike proof"
      contains: "list_semantic_views"
    - path: "test/integration/test_concurrent_reads_per_call_conn.py"
      provides: "Concurrent per-call connection regression"
      contains: "SHOW SEMANTIC DIMENSIONS"
  key_links:
    - from: "Each migrated bind callback (17 total)"
      to: "Connection(*context.db)"
      via: "C++ bind opens per-call Connection, FFI bridge to Rust"
      pattern: "Connection probe\\(\\*context\\.db\\)"
    - from: "src/lib.rs (post-H2-retirement)"
      to: "[nothing]"
      via: "no long-lived query_conn allocation"
      pattern: "let mut query_conn"
---

<objective>
Migrate all 17 read-side function registrations (15 table functions + 2 scalars per the locked count) from duckdb-rs `register_table_function_with_extra_info` / `register_scalar_function_with_state` to the C++ Catalog API shim `sv_register_table_function` (introduced in Plan 04). For each, the new bind callback opens a per-call `Connection(*context.db)` on the C++ side, bridges to Rust to invoke the existing bind/exec logic, and drops the connection at end of bind scope. Defer type inference (LIMIT 0 probes + `typeof` lookups) from CREATE-time (already removed in Plan 03) to read-side bind time, backed by an unbounded process-local HashMap cache keyed on (view_name, schema_fingerprint). The final commit retires H2 query_conn at src/lib.rs:498-507.

Wave 0 is a Rust↔C++ bridge spike (A3 resolution): confirms Rust callbacks reachable from a C++ bind callback registered via `sv_register_table_function`, and confirms `Connection(*context.db)` is usable from the bridged callback. One migrated callback (list_semantic_views -- simplest, no args) serves as the end-to-end proof.

Waves 1-6 migrate callbacks in the order RESEARCH §1.3 recommends (simplest first, semantic_view last because it has the highest blast radius). The final atomic commit deletes the H2 query_conn allocation and the QueryState struct.

Purpose: This is the second of the two long-lived-handle retirements that fix the LIFE-01 in-process RW→RO hang. After Plan 05, every read-side function opens its own per-call connection at bind time; no read path consumes a stashed extension-owned `duckdb_connection`. Plan 06 then retires H1 (catalog_conn) and the structural guard test ensures no future regression re-introduces either pattern.

Output: 17 migrated registrations on the C++ Catalog API; a process-local type cache; H2 retired; existing read-side behavior byte-identical at the user-visible level; new concurrent-read regression coverage.
</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/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-04-SUMMARY.md

<!-- Spike evidence -->
@.planning/phases/65-overridecontext-connection-teardown/65-READ-PATH-SPIKE.md
@.planning/phases/65-overridecontext-connection-teardown/65-OPTION-B-SPIKE.md
@.planning/phases/65-overridecontext-connection-teardown/65-EXEC-TIME-SPIKE.md

<!-- C++ shim already introduced in Plan 04; Plan 05 extends -->
@cpp/src/shim.cpp
@cpp/src/shim.hpp

<!-- 17 read-side function source files (see <interfaces> for full inventory) -->
@src/lib.rs
@src/ddl/list.rs
@src/ddl/describe.rs
@src/ddl/show_columns.rs
@src/ddl/show_dims.rs
@src/ddl/show_dims_for_metric.rs
@src/ddl/show_metrics.rs
@src/ddl/show_facts.rs
@src/ddl/show_materializations.rs
@src/ddl/get_ddl.rs
@src/ddl/read_yaml.rs
@src/query/table_function.rs
@src/query/explain.rs
@src/catalog.rs

<!-- Reusable type inference -->
@src/query/table_function.rs

<!-- Tests that must stay green -->
@test/integration/test_adbc_transactions.py
@test/integration/test_multi_db_isolation.py
@test/integration/test_concurrent_ddl.py
@test/sql/phase41_describe.test

<interfaces>
<!-- Read-side callback inventory (17 total per RESEARCH §3) -->

| # | Function | Type | Source file | Args | Notes |
|---|----------|------|-------------|------|-------|
| 1 | list_semantic_views | TF | src/ddl/list.rs (ListSemanticViewsVTab) | () | Wave 0 spike target -- simplest |
| 2 | list_terse_semantic_views | TF | src/ddl/list.rs (ListTerseSemanticViewsVTab) | () | Wave 1 |
| 3 | show_columns_in_semantic_view | TF | src/ddl/show_columns.rs | (VARCHAR) | Wave 2 |
| 4 | describe_semantic_view | TF | src/ddl/describe.rs | (VARCHAR) | Wave 2; D-03b post-reopen target |
| 5 | show_semantic_dimensions | TF | src/ddl/show_dims.rs (ShowSemanticDimensionsVTab) | (VARCHAR) | Wave 2; D-03b post-reopen target |
| 6 | show_semantic_dimensions_all | TF | src/ddl/show_dims.rs (ShowSemanticDimensionsAllVTab) | () | Wave 1 |
| 7 | show_semantic_dimensions_for_metric | TF | src/ddl/show_dims_for_metric.rs | (VARCHAR, VARCHAR) | Wave 3 |
| 8 | show_semantic_metrics | TF | src/ddl/show_metrics.rs (ShowSemanticMetricsVTab) | (VARCHAR) | Wave 2 |
| 9 | show_semantic_metrics_all | TF | src/ddl/show_metrics.rs (ShowSemanticMetricsAllVTab) | () | Wave 1 |
| 10 | show_semantic_facts | TF | src/ddl/show_facts.rs (ShowSemanticFactsVTab) | (VARCHAR) | Wave 2 |
| 11 | show_semantic_facts_all | TF | src/ddl/show_facts.rs (ShowSemanticFactsAllVTab) | () | Wave 1 |
| 12 | show_semantic_materializations | TF | src/ddl/show_materializations.rs (ShowSemanticMaterializationsVTab) | (VARCHAR) | Wave 2 |
| 13 | show_semantic_materializations_all | TF | src/ddl/show_materializations.rs (ShowSemanticMaterializationsAllVTab) | () | Wave 1 |
| 14 | get_ddl | Scalar | src/ddl/get_ddl.rs (GetDdlScalar) | (VARCHAR) | Wave 4; D-03b post-reopen target |
| 15 | read_yaml_from_semantic_view | Scalar | src/ddl/read_yaml.rs (ReadYamlFromSemanticViewScalar) | (VARCHAR) | Wave 4 |
| 16 | semantic_view | TF | src/query/table_function.rs (SemanticViewVTab) | varargs | Wave 6 -- LAST migration; D-03b post-reopen target |
| 17 | explain_semantic_view | TF | src/query/explain.rs (ExplainSemanticViewVTab) | varargs | Wave 5 |

<!-- Bridge pattern (per-call Connection from C++ bind, threaded through to Rust) -->

```cpp
static unique_ptr<FunctionData> sv_<name>_bind(
    ClientContext &context, TableFunctionBindInput &input,
    vector<LogicalType> &return_types, vector<string> &names) {
    auto bd = make_uniq<MyBindData>();
    Connection probe(*context.db);
    // Convert the C++ Connection's internal conn pointer to a duckdb_connection
    // C-API handle for Rust consumption. See A3 spike for the exact mechanism.
    duckdb_connection raw_conn = /* from probe -- bridge mechanism per A3 spike */;
    // Call the Rust bind dispatcher
    sv_<vtab>_bind_rust(raw_conn, /* args from input.inputs */, /* return_types/names ptrs */);
    return std::move(bd);
}
```

<!-- Process-local type cache shape (src/type_cache.rs) -->

```rust
use std::collections::HashMap;
use std::sync::{Arc, OnceLock, RwLock};

#[derive(Clone, Debug)]
pub struct InferredTypes {
    pub column_types: Vec<(String, String)>,  // (column_name, type_name)
}

static TYPE_CACHE: OnceLock<RwLock<HashMap<(String, u64), Arc<InferredTypes>>>> = OnceLock::new();

pub fn lookup_or_probe(
    view_name: &str,
    fingerprint: u64,
    probe: impl FnOnce() -> Result<InferredTypes, String>,
) -> Result<Arc<InferredTypes>, String> { /* ... */ }
```

The `schema_fingerprint` is a stable hash (`std::collections::hash_map::DefaultHasher` or a deterministic crate) of the relevant `SemanticViewDefinition` fields (tables, dimensions, metrics, facts) -- ensures that ALTER invalidates the cache naturally via JSON change.

NOT a bounded LRU. TECH-DEBT 20 is the anti-pattern reference.

<!-- H2 retirement target (Wave 6 final commit) -->

src/lib.rs:498-507 -- the entire block to delete:

```rust
// Create a NEW connection for the semantic_view table function.
let mut query_conn: ffi::duckdb_connection = ptr::null_mut();
let rc = unsafe { ffi::duckdb_connect(db_handle, &mut query_conn) };
if rc != ffi::DuckDBSuccess {
    return Err("Failed to create query connection for semantic_view".into());
}
let query_state = QueryState {
    catalog: catalog_reader,
    conn: query_conn,
};
```

`QueryState` struct at src/query/table_function.rs:33-37 also retires (or refactors to a per-call shape).
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1 (Wave 0): Rust↔C++ bridge spike for read-path callbacks</name>
  <files>cpp/src/shim.cpp, cpp/src/shim.hpp, src/ddl/list.rs, src/lib.rs, test/sql/65_read_bridge_spike.test, test/sql/TEST_LIST</files>
  <read_first>
    - 65-CONTEXT.md "Resolutions during plan-phase" section A3
    - 65-RESEARCH.md §1.3 Option A (Rust↔C++ bridge) -- the only viable mechanism per the spike evidence; §3 (per-callback inventory + common bind-callback pattern); §5 (C++ Catalog API patterns); §12 "Risks and Known-Falsified Alternatives" (Options B and C ruled out)
    - 65-PATTERNS.md "Plan 05 -- read-path migration wave" section, especially "Rust↔C++ bridge callback dispatcher" at lines 476-480
    - 65-READ-PATH-SPIKE.md (entire file -- this is the bind-callback evidence base; lines 21-31 are the registration template; the spike never exercised the Rust callback path so the bridge mechanism is unverified)
    - 65-OPTION-B-SPIKE.md (Connection(*context.db) works from extension callbacks)
    - 65-EXEC-TIME-SPIKE.md (context.Query alternatives self-deadlock -- bridge MUST use Connection, not context.Query)
    - src/ddl/list.rs::ListSemanticViewsVTab (current shape, lines 51-100) -- the simplest TF to migrate, perfect for the spike
    - src/parse.rs::sv_parser_override_rust (catch_unwind + ptr+len FFI pattern -- analog for the bridge dispatcher)
    - cpp/src/shim.cpp (current state including sv_register_table_function from Plan 04)
    - Context7 docs for DuckDB v1.5 `Connection` C++ class: look for the internal pointer or accessor that returns a `duckdb_connection` C-API handle equivalent (`ctx7 library duckdb` then query "Connection internal pointer duckdb_connection")
  </read_first>
  <behavior>
    - Test 1: After bridge code lands, a single sqllogictest `SELECT * FROM list_semantic_views()` against a DB with 0 / 1 / 5 semantic views returns the same rows as the pre-migration shape, byte-identical.
    - Test 2: The bridge mechanism opens `Connection probe(*context.db)` inside the C++ bind callback (verified by lldb breakpoint OR by an `assert(connection_count > 0)` probe). After bind returns, the per-call Connection drops; subsequent calls each open their own.
    - Test 3: A Rust panic inside the bridged callback does not propagate across the C++ boundary -- bind throws a BinderException with the panic message; the SQL query returns an error not a process crash.
    - Test 4 (negative control): The pre-migration list_semantic_views registration via `register_table_function_with_extra_info` is REMOVED from src/lib.rs by the end of this task (the spike's success means the migration must be permanent, not a parallel registration).
  </behavior>
  <action>
**Step A -- Bridge mechanism design:** Examine DuckDB's `Connection` C++ class to identify the cleanest way to obtain a `duckdb_connection` C-API handle from a stack-constructed `Connection probe(*context.db)`. Options to spike:
  1. The `Connection` class exposes a `context` member that is a `shared_ptr<ClientContext>` -- the C-API `duckdb_connection` wraps a similar pointer. Look for an internal accessor.
  2. The C-API `duckdb_connect(db, &out)` itself constructs a Connection -- but we know it fails (BIND-THREAD-RC1) when called from extension callbacks. We need a different path.
  3. Construct a `duckdb_connection` wrapper struct manually around the C++ Connection's internals, casting where necessary. Per RESEARCH §12 the C++ direct path works -- find the exact incantation that wraps it into the C-API surface the Rust side expects.

The PATTERNS.md "no analog found" entry (line 707) confirms this is a new FFI surface; document the chosen mechanism in 65-05-SUMMARY.md.

If none of the above works, fall back to: do NOT bridge via duckdb_connection at all. Instead expose a Rust trait object or function pointer that takes a `*mut Connection_cpp_opaque` and route catalog queries through C++ helper functions called from Rust (e.g. `extern "C" const char *sv_query_str(Connection *, const char *sql, size_t *out_len)`). The Rust side then never sees a duckdb_connection -- it just sees an opaque pointer to a query helper. This is the "rewrite read-side bodies in C++" fallback flagged in RESEARCH §1.3 A3 risk note.

Prefer the duckdb_connection bridge if at all viable -- it minimizes the migration delta for the other 16 callbacks.

**Step B -- Implement the bridge for list_semantic_views:** Replace the `register_table_function_with_extra_info` registration for `list_semantic_views` in src/lib.rs with a call to `sv_register_table_function`. In cpp/src/shim.cpp add `sv_list_semantic_views_bind` + `sv_list_semantic_views_function`. The bind callback opens `Connection probe(*context.db)`, derives the bridge handle, calls `sv_list_semantic_views_bind_rust(...)` (new Rust FFI helper in src/ddl/list.rs), copies the result into BindData.

The Rust FFI helper `sv_list_semantic_views_bind_rust` follows the catch_unwind + ptr+len pattern: receives the per-call connection handle, wraps it in a local `CatalogReader::new(conn, true)`, calls `reader.list_all()`, serializes the result rows into a heap-owned buffer for the C++ side to copy. The exec callback then emits these rows into DataChunk.

**Step C -- Test:** Add test/sql/65_read_bridge_spike.test asserting `SELECT * FROM list_semantic_views()` returns the right schema and rows against a DB with 0 / 1 / 3 semantic views. Also assert the function name still appears in `duckdb_functions()` (registration is visible).

Run `just test-sql` -- ALL existing tests that call list_semantic_views (e.g. test/sql/phase42_persistence.test or wherever it's used) must continue to pass byte-identical.

Append `65_read_bridge_spike.test` to test/sql/TEST_LIST.

**Halt-on-failure gate:** If after Step A the bridge mechanism doesn't work (e.g. the chosen accessor doesn't compile or causes UB at runtime), halt the plan and report the failure to the user. The fallback "C++ helper trait object" path needs user approval before adopting because it materially changes the Plan 05 LOC budget (all 17 callbacks need helper wrappers, not just FFI bridge adapters).
  </action>
  <verify>
    <automated>just build 2>&amp;1 | tail -10 ; just test-sql test/sql/65_read_bridge_spike.test 2>&amp;1 | tail -20 ; just test-sql 2>&amp;1 | tail -20 ; cargo test --features extension --no-default-features list 2>&amp;1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `just build` exits 0; no undefined symbols or link errors
    - `just test-sql test/sql/65_read_bridge_spike.test` exits 0
    - `just test-sql` (all suites) exits 0 -- no regressions in any existing test that calls list_semantic_views
    - `grep -nE 'register_table_function_with_extra_info.*list_semantic_views' src/lib.rs` returns 0 matches (the old registration is GONE; the new one is via sv_register_table_function)
    - `grep -c 'sv_list_semantic_views_bind' cpp/src/shim.cpp` returns at least 1
    - `grep -c 'sv_list_semantic_views_bind_rust' src/ddl/list.rs` returns at least 1
    - The bridge mechanism is documented in src/ddl/list.rs (Rust side) or cpp/src/shim.cpp (C++ side) with a comment block explaining the chosen approach + why it doesn't trigger BIND-THREAD-RC1
    - A deliberate panic in the Rust bind dispatcher is caught and surfaced as a SQL error, not a process crash (manual smoke test acceptable)
  </acceptance_criteria>
  <done>
The Rust↔C++ bridge mechanism is empirically validated against one migrated callback. The 16 remaining migrations can adopt the same pattern. If the bridge spike fails, the plan halts pending user direction on the fallback path.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2 (Wave 1): Introduce sv_register_scalar_function shim + process-local type cache; migrate the 5 zero-arg TF "all" siblings</name>
  <files>cpp/src/shim.cpp, cpp/src/shim.hpp, src/type_cache.rs, src/lib.rs, src/ddl/list.rs, src/ddl/show_dims.rs, src/ddl/show_metrics.rs, src/ddl/show_facts.rs, src/ddl/show_materializations.rs</files>
  <read_first>
    - 65-CONTEXT.md `<decisions>` D-14, D-16, D-17
    - 65-RESEARCH.md §1.3 (sequencing); §5.3 community-extension scalar-function registration; §6 (type inference deferral + cache shape)
    - 65-PATTERNS.md "Plan 05" section (lines 439-518); "Process-local type-inference cache" (lines 482-492)
    - cpp/src/shim.cpp (with sv_register_table_function from Plan 04 and the list_semantic_views bridge from Task 1)
    - The 5 zero-arg "_all" TF sources: src/ddl/list.rs::ListTerseSemanticViewsVTab + src/ddl/show_dims.rs::ShowSemanticDimensionsAllVTab + src/ddl/show_metrics.rs::ShowSemanticMetricsAllVTab + src/ddl/show_facts.rs::ShowSemanticFactsAllVTab + src/ddl/show_materializations.rs::ShowSemanticMaterializationsAllVTab
    - src/lib.rs (current registrations for all 5 to migrate at the appropriate line ranges per RESEARCH §3 inventory)
    - DuckDB v1.5 C++ scalar function registration pattern: `system_catalog.CreateFunction(txn, ScalarFunctionSet)` -- use Context7 to confirm exact signature on v1.5.2 (`ctx7 library duckdb` then query "ScalarFunctionSet CreateFunction system_catalog")
  </read_first>
  <behavior>
    - Test 1: After migration, `SELECT * FROM list_terse_semantic_views()` returns identical rows to pre-migration.
    - Test 2: Same for `show_semantic_dimensions_all`, `show_semantic_metrics_all`, `show_semantic_facts_all`, `show_semantic_materializations_all`.
    - Test 3: Type cache cold call: probe runs once; second call within the same process for the same (view_name, fingerprint) hits the cache (verified by a counter or an explicit log assertion).
    - Test 4: ALTER on a view invalidates the cache for that view's old fingerprint (because the JSON changed, the new fingerprint is different -- old entry stays in cache as stale but unreachable; new probe runs on first hit).
    - Test 5: sv_register_scalar_function is callable from Rust and registers a one-arg scalar (use a no-op fixture scalar for the smoke test, e.g. `__sv_test_scalar(x) → x` which echoes its input; remove the fixture after smoke).
    - Test 6: phase39_metadata_storage.test, phase42_persistence.test, and any other test exercising the 5 migrated "_all" TFs all pass byte-identical.
  </behavior>
  <action>
**Step A -- Introduce sv_register_scalar_function in cpp/src/shim.cpp:**

Following the same template as sv_register_table_function (Plan 04 Task 2), build a C-callable wrapper around `system_catalog.CreateFunction(txn, ScalarFunctionSet(name, ...))`. Signature suggestion:

```cpp
extern "C" bool sv_register_scalar_function(
    duckdb_database db_handle,
    const char *name,
    const LogicalType *arg_types, size_t arg_count,
    LogicalType return_type,
    scalar_function_t exec_cb);
```

Use Context7 (`ctx7 library duckdb`) if the exact signature on v1.5.2 differs. Confirm against one community extension's scalar registration if possible (postgres_scanner is a candidate per RESEARCH §5.3).

Declare in cpp/src/shim.hpp.

**Step B -- Introduce src/type_cache.rs:**

Create the module with the shape from the `<interfaces>` block. Public functions:
  - `pub fn fingerprint(def: &SemanticViewDefinition) -> u64` -- deterministic hash of the relevant fields (tables, dimensions, metrics, facts). Use `std::collections::hash_map::DefaultHasher` for now per A5 resolution (low risk).
  - `pub fn lookup_or_probe(view_name: &str, fingerprint: u64, probe: impl FnOnce() -> Result<InferredTypes, String>) -> Result<Arc<InferredTypes>, String>` -- atomic check-and-insert. Acquire read lock first; if miss, acquire write lock and probe under lock to prevent duplicate probes.
  - The cache MUST NOT be a bounded LRU. Document the TECH-DEBT 20 anti-pattern in a module-level comment.

Add the module declaration to src/lib.rs (`pub mod type_cache;` -- top of file with other module declarations).

Add unit tests in src/type_cache.rs::tests for cold lookup, hit, fingerprint invalidation.

**Step C -- Migrate the 5 zero-arg "_all" TFs:**

For each of (list_terse_semantic_views, show_semantic_dimensions_all, show_semantic_metrics_all, show_semantic_facts_all, show_semantic_materializations_all):

1. In cpp/src/shim.cpp, add `sv_<name>_bind` + `sv_<name>_function` following the Task 1 list_semantic_views template. Bind opens `Connection probe(*context.db)`, calls into Rust dispatcher, copies result into BindData. Exec emits rows.
2. In the corresponding src/ddl/*.rs file, add `sv_<name>_bind_rust` and any auxiliary helper. Reuse the existing bind/exec body but take a per-call duckdb_connection argument (via the Task 1 bridge mechanism) instead of accessing `bind.get_extra_info::<CatalogReader>()`. The function body inside becomes byte-identical with the current shape from `CatalogReader::new(per_call_conn, true).list_all()` (or the equivalent for SHOW operations) onward.
3. In src/lib.rs, replace the `register_table_function_with_extra_info` call for that VTab with `sv_register_table_function(db_handle, name, args, bind_cb, exec_cb, init_cb)`.

After all 5 are migrated, run `just test-sql` and `cargo test`. All existing tests calling these functions must pass byte-identical.

**Note on type cache integration in Wave 1:** the 5 zero-arg "_all" TFs do NOT directly need the type cache yet -- they return only metadata, not inferred column types. The cache is introduced in Wave 1 for use by Wave 2-6 callbacks (describe, semantic_view, etc.). Adding it now keeps Wave 1 self-contained and lets Wave 2+ build on top.
  </action>
  <verify>
    <automated>just build 2>&amp;1 | tail -10 ; just test-sql 2>&amp;1 | tail -20 ; cargo test --features extension --no-default-features type_cache 2>&amp;1 | tail -10 ; uv run test/integration/test_adbc_transactions.py 2>&amp;1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `just build` exits 0
    - `just test-sql` exits 0; all existing tests for the 5 migrated TFs pass byte-identical
    - `cargo test --features extension --no-default-features type_cache::tests` exits 0; cache unit tests pass
    - `uv run test/integration/test_adbc_transactions.py` exits 0 (D-21 invariant)
    - `grep -c 'sv_register_scalar_function' cpp/src/shim.cpp` returns at least 1 (definition)
    - `grep -c 'sv_register_scalar_function' cpp/src/shim.hpp` returns at least 1 (declaration)
    - `grep -nE 'register_table_function_with_extra_info.*(list_terse|dimensions_all|metrics_all|facts_all|materializations_all)' src/lib.rs` returns 0 matches (all 5 old registrations are gone)
    - `test -f src/type_cache.rs` returns 0 (file exists)
    - `grep -c 'OnceLock' src/type_cache.rs` returns at least 1; `grep -c 'LRU' src/type_cache.rs` returns 0 (anti-pattern enforcement)
  </acceptance_criteria>
  <done>
sv_register_scalar_function is in place for Wave 4. Type cache module exists with unit tests. The 5 zero-arg "_all" TFs are migrated. Wave 2 can now adopt the same pattern for the name-arg TF family.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3 (Wave 2 + 3): Migrate the 6 name-arg TFs + the 2-arg show_semantic_dimensions_for_metric</name>
  <files>cpp/src/shim.cpp, src/lib.rs, src/ddl/show_columns.rs, src/ddl/describe.rs, src/ddl/show_dims.rs, src/ddl/show_dims_for_metric.rs, src/ddl/show_metrics.rs, src/ddl/show_facts.rs, src/ddl/show_materializations.rs</files>
  <read_first>
    - 65-RESEARCH.md §1.3 Waves 2-3
    - 65-PATTERNS.md "17 read-side bind-callback refactors" section (lines 446-474)
    - The 7 source files for the name-arg + 2-arg TFs (see inventory in `<interfaces>`)
    - src/lib.rs current registrations at the relevant lines per RESEARCH §3 inventory (433, 437, 443, 453, 459, 469, 479)
    - The type cache module from Task 2 -- describe_semantic_view + show_columns_in_semantic_view + semantic_view (later) need to probe types lazily per D-16
  </read_first>
  <behavior>
    - Test 1: `SELECT * FROM show_columns_in_semantic_view('v')`, `describe_semantic_view('v')`, `show_semantic_dimensions('v')`, etc. all return identical rows to pre-migration.
    - Test 2: For describe / show_columns / show_semantic_dimensions, the data_type column is populated on first call (probe runs); subsequent calls within the same process serve from cache.
    - Test 3: `SELECT * FROM show_semantic_dimensions_for_metric('v', 'metric_name')` returns identical rows pre/post migration.
    - Test 4: Cross-database call (ATTACH 'b.db' AS db2; SELECT * FROM describe_semantic_view('db2.main.v')) works correctly because the per-call Connection inherits the caller's catalog/schema search path.
    - Test 5: phase41_describe.test continues to pass byte-identical with no edits.
  </behavior>
  <action>
For each of the 7 functions in this task:
  - show_columns_in_semantic_view (lib.rs:433)
  - describe_semantic_view (lib.rs:437)
  - show_semantic_dimensions (lib.rs:443)
  - show_semantic_dimensions_for_metric (lib.rs:453)
  - show_semantic_metrics (lib.rs:459)
  - show_semantic_facts (lib.rs:469)
  - show_semantic_materializations (lib.rs:479)

Follow the Task 2 migration template:
  1. Add `sv_<name>_bind` + `sv_<name>_function` in cpp/src/shim.cpp. For 6 functions take one VARCHAR arg (view name); for show_semantic_dimensions_for_metric take two VARCHAR args.
  2. Add `sv_<name>_bind_rust` in the corresponding src/ddl/*.rs. Takes per-call conn + view name (+ metric name for show_dims_for_metric). For describe, show_columns, show_semantic_dimensions: integrate the type cache -- after looking up the SemanticViewDefinition, compute fingerprint, call `type_cache::lookup_or_probe(view_name, fingerprint, || try_infer_schema(conn, &limit0_sql))`. The Result populates data_type / etc. in the output rows.
  3. Replace the `register_table_function_with_extra_info` call in src/lib.rs with `sv_register_table_function`.

After all 7 are migrated, the migration covers 12 of 17 functions (Wave 1's 6 + this task's 7 minus the 1 already-migrated from Wave 0 = 12). The remaining 5 are: get_ddl, read_yaml_from_semantic_view (scalars, Task 4), explain_semantic_view (Task 5), semantic_view (Task 6).

Verify `just test-sql` and `just test-all` are green. The `just test-sql test/sql/phase41_describe.test` is the most sensitive test surface -- describe_semantic_view's behavior must be byte-identical.

Pay attention to the type-cache integration: if a CREATE-time persisted JSON had `column_type_names: [...]` from a pre-v0.10.0 install, the bind callback should NOT use that field (D-16/17 -- types are read-side only now). Verify the bind ignores any persisted types and always either hits cache or probes. The existing v0.7.1 behavior of "populated on first DESCRIBE" is restored exactly because we re-probe.
  </action>
  <verify>
    <automated>just build 2>&amp;1 | tail -10 ; just test-sql 2>&amp;1 | tail -20 ; just test-sql test/sql/phase41_describe.test 2>&amp;1 | tail -10 ; uv run test/integration/test_adbc_transactions.py 2>&amp;1 | tail -10 ; uv run test/integration/test_multi_db_isolation.py 2>&amp;1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `just build` exits 0
    - `just test-sql` exits 0; all existing tests for the 7 migrated functions pass byte-identical
    - `just test-sql test/sql/phase41_describe.test` exits 0 (high-sensitivity surface)
    - `uv run test/integration/test_adbc_transactions.py` exits 0 (D-21 invariant)
    - `uv run test/integration/test_multi_db_isolation.py` exits 0 (cross-database catalog/schema resolution works because per-call Connection inherits caller's search path)
    - `grep -nE 'register_table_function_with_extra_info' src/lib.rs` returns at most 4 matches (the remaining 5 functions to migrate in Tasks 4-6 + the 1 from Task 1 already migrated = 17 total - 12 migrated = 5 left -- actually: list_semantic_views (Task 1), 5 from Task 2, 7 from this Task = 13 migrated; 4 left: get_ddl scalar + read_yaml scalar + explain_semantic_view + semantic_view). Verify count.
    - describe_semantic_view's data_type column is populated on first call via the type cache (verify via a tiny test that calls describe twice and asserts the second call is faster or hits the cache log)
  </acceptance_criteria>
  <done>
13 of 17 read-side functions are migrated to the C++ Catalog API shim. Type inference works via the process-local cache, lazily probed at bind time. Cross-database scenarios resolve correctly on the caller's connection. Phase41 + Phase42 test surfaces stay byte-identical.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 4 (Wave 4): Migrate the 2 scalars (get_ddl, read_yaml_from_semantic_view)</name>
  <files>cpp/src/shim.cpp, src/lib.rs, src/ddl/get_ddl.rs, src/ddl/read_yaml.rs</files>
  <read_first>
    - 65-RESEARCH.md §1.3 Wave 4 (scalar registration via sv_register_scalar_function from Task 2)
    - 65-PATTERNS.md "sv_register_scalar_function" mention in "Plan 05" header (lines 41-42)
    - src/ddl/get_ddl.rs::GetDdlScalar (current shape -- one-arg VARCHAR returning VARCHAR DDL string)
    - src/ddl/read_yaml.rs::ReadYamlFromSemanticViewScalar (current shape -- one-arg VARCHAR returning VARCHAR YAML string)
    - src/lib.rs registration sites at lines 489 (get_ddl) and 492 (read_yaml)
    - The sv_register_scalar_function shim introduced in Task 2 Step A
  </read_first>
  <behavior>
    - Test 1: `SELECT get_ddl('v')` returns identical DDL string to pre-migration.
    - Test 2: `SELECT read_yaml_from_semantic_view('v')` returns identical YAML round-trip.
    - Test 3: phase50_get_ddl-style and phase56_yaml_export-style tests (whichever exist) pass byte-identical.
    - Test 4: Both scalars work in the ATTACH scenario (caller's search path inherited via per-call Connection).
  </behavior>
  <action>
Apply the Task 2 migration pattern but for scalars:

1. In cpp/src/shim.cpp, add `sv_get_ddl_exec` and `sv_read_yaml_from_semantic_view_exec` scalar exec callbacks. Each gets `ExecuteFunction(DataChunk &args, ExpressionState &state, Vector &result)`. Inside: for each row, extract the view name VARCHAR arg, open `Connection probe(*state.GetContext().db)`, call into Rust via the bridge, place the result string into the `result` Vector via FlatVector::SetData / SetValue. Per-row Connection construction is fine -- scalars typically process few rows for our use cases (get_ddl on a small set of views).

   Alternative if per-row Connection is too costly: amortize by opening one Connection per chunk (DataChunk has up to STANDARD_VECTOR_SIZE rows ~2048) and reusing across the row loop. Use whichever is simpler and benchmark only if test latency becomes an issue.

2. In src/ddl/get_ddl.rs and src/ddl/read_yaml.rs, add Rust FFI dispatchers `sv_get_ddl_exec_rust` and `sv_read_yaml_exec_rust` that take per-call conn + name + return a UTF-8 owned buffer.

3. In src/lib.rs replace the `register_scalar_function_with_state` calls with `sv_register_scalar_function(db_handle, name, args, return_type, exec_cb)`.

Verify `just test-sql` and any existing tests using get_ddl or read_yaml_from_semantic_view pass byte-identical.
  </action>
  <verify>
    <automated>just build 2>&amp;1 | tail -10 ; just test-sql 2>&amp;1 | tail -20 ; cargo test --features extension --no-default-features 2>&amp;1 | tail -10 ; uv run test/integration/test_adbc_transactions.py 2>&amp;1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `just build` exits 0
    - `just test-sql` exits 0
    - `uv run test/integration/test_adbc_transactions.py` exits 0
    - `grep -nE 'register_scalar_function_with_state' src/lib.rs` returns 0 matches (both scalars migrated)
    - get_ddl round-trip test (CREATE → get_ddl → re-CREATE → describe) returns identical DDL
    - read_yaml_from_semantic_view returns YAML that can be re-parsed and re-CREATEd via CREATE FROM YAML FILE (round-trip)
  </acceptance_criteria>
  <done>
Both scalars are migrated via sv_register_scalar_function. 15 of 17 functions now on the C++ Catalog API. Waves 5 and 6 migrate the highest-blast-radius remaining two (explain_semantic_view and semantic_view).
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 5 (Wave 5): Migrate explain_semantic_view</name>
  <files>cpp/src/shim.cpp, src/lib.rs, src/query/explain.rs</files>
  <read_first>
    - 65-RESEARCH.md §1.3 Wave 5
    - src/query/explain.rs::ExplainSemanticViewVTab (current shape -- varargs)
    - src/lib.rs registration at line 515
    - The semantic_view function it mirrors (src/query/table_function.rs::SemanticViewVTab) -- explain shares the bind/expand logic but emits the plan instead of executing it
  </read_first>
  <behavior>
    - Test 1: `SELECT * FROM explain_semantic_view('v', dimensions := ['x'], metrics := ['y'])` returns the same expanded SQL plan as pre-migration.
    - Test 2: ATTACH 'b.db' AS db2; SELECT * FROM explain_semantic_view('db2.main.v', ...) resolves on the caller's connection correctly.
    - Test 3: D-20 invariant: the expanded plan uses qualify_and_quote_table_ref output (per Phase 64 wiring at src/expand/sql_gen.rs:499,530,550) -- no regression.
  </behavior>
  <action>
Apply the Task 3 migration template for explain_semantic_view. The varargs nature means the bind callback extracts the view name and the optional `dimensions :=` / `metrics :=` / `facts :=` named arguments from `input.named_parameters`. Per-call `Connection(*context.db)` is opened, the Rust dispatcher receives the conn + name + named args, calls into the existing expand() pipeline (read-only), returns the EXPLAIN output.

Verify `just test-sql` for any phase that exercises explain_semantic_view (likely phase57_introspection_diagnostics.test or similar). Pass byte-identical.

Regression check: Phase 64 qualify_and_quote_table_ref call sites at src/expand/sql_gen.rs:499,530,550 are NOT touched by this migration (the expand pipeline is unchanged) but the test surface should confirm by running test/sql/64_quoted_identifier.test (or whatever phase64 tests are named) and asserting they pass byte-identical.
  </action>
  <verify>
    <automated>just build 2>&amp;1 | tail -10 ; 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>
    - `just build` exits 0
    - `just test-sql` exits 0; all phases exercising explain_semantic_view pass byte-identical
    - Phase 64 test surface passes byte-identical (qualify_and_quote_table_ref call sites not regressed)
    - `grep -nE 'register_table_function_with_extra_info' src/lib.rs` returns at most 1 match (only semantic_view remains; migrated in Task 6)
  </acceptance_criteria>
  <done>
16 of 17 functions migrated. Only semantic_view (main expansion path) remains -- Task 6 migrates it and retires H2.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 6 (Wave 6): Migrate semantic_view (main expansion path); retire H2 query_conn; final phase gate</name>
  <files>cpp/src/shim.cpp, src/lib.rs, src/query/table_function.rs, src/catalog.rs, test/integration/test_concurrent_reads_per_call_conn.py, test/sql/TEST_LIST</files>
  <read_first>
    - 65-CONTEXT.md `<decisions>` D-15 (H2 retirement is final atomic step), D-20 (Phase 64 qualify wiring no regression), D-21 (transactional invariant)
    - 65-RESEARCH.md §1.3 Wave 6 + §9 H1/H2 retirement map
    - 65-PATTERNS.md "src/lib.rs:498-507 H2 retirement" (lines 498-512) + "test/integration/test_concurrent_reads_per_call_conn.py" (lines 516-518)
    - src/query/table_function.rs::SemanticViewVTab (current shape -- varargs, main expansion path through expand::expand) + lines 33-37 QueryState struct
    - src/lib.rs:498-507 (H2 query_conn allocation block to delete in final commit) + the broader 425-518 read-side registration block
    - src/catalog.rs (CatalogReader struct -- post-Plan-05 should be per-call instantiated; review the constructor signature)
    - src/expand/sql_gen.rs lines 499, 530, 550 (Phase 64 qualify_and_quote_table_ref call sites -- D-20 must not regress)
    - test/integration/test_multi_db_isolation.py (existing concurrent-DB scenarios -- must stay green)
    - test/integration/test_concurrent_ddl.py (existing concurrent-Python-threads pattern -- analog for test_concurrent_reads_per_call_conn.py)
  </read_first>
  <behavior>
    - Test 1: `SELECT * FROM semantic_view('v', dimensions := [...], metrics := [...])` returns identical rows pre/post migration.
    - Test 2: ATTACH 'b.db' AS db2; SELECT * FROM semantic_view('db2.main.v', ...) returns rows (the H2 catalog-search-path divergence root cause expected to dissolve here -- if it does, Phase 66's EXPAND-CTX-01 may be a no-op verification).
    - Test 3: BEGIN; SELECT ... FROM semantic_view(...); ROLLBACK -- the SELECT runs on the caller's connection, no transaction state crossed; existing transactional tests stay green (D-21).
    - Test 4: After Task 6's final commit, src/lib.rs:498-507 (H2 query_conn allocation) is GONE; `grep -n 'let mut query_conn' src/lib.rs` returns 0 matches.
    - Test 5: QueryState struct retired (or refactored to take per-call conn at method-call time).
    - Test 6: 8 parallel SHOW SEMANTIC DIMENSIONS calls in test_concurrent_reads_per_call_conn.py each succeed with their own per-call connection (no shared mutable state contention).
    - Test 7: Phase 64 sqllogictest (qualify_and_quote_table_ref wired sites) pass byte-identical.
    - Test 8: `just test-all` and `just ci` both green on milestone/v0.10.0.
  </behavior>
  <action>
**Step A -- Migrate semantic_view:**

Apply the same migration template as Task 5 for explain_semantic_view but for src/query/table_function.rs::SemanticViewVTab. The bind callback extracts the view name + named args; opens `Connection probe(*context.db)`; calls into Rust via the bridge; the Rust side runs the existing expand::expand() pipeline (read-only) to compute the SQL plan, opens a per-call SQL preparer (or runs the query inline) and emits rows.

The exec callback may need to invoke a sub-query on the same per-call Connection (the expanded SQL is what produces the actual rows). Confirm the bind/exec architecture matches what duckdb-rs's table function shape demanded -- the C++ Catalog API path may allow returning a `LogicalProjection` directly or may require re-invoking the prepared SQL. Use 65-OPTION-B-SPIKE.md's PLAN-THREAD-RC0 evidence: `Connection(*context.db).Query(...)` works from the plan thread.

**Step B -- Verify semantic_view migration in isolation:**

Before deleting H2, run `just test-sql` and any test exercising semantic_view (every phase 25-onwards has tests). All must pass byte-identical.

If any cross-database test (test_multi_db_isolation.py) fails because the catalog-search-path divergence persists despite the per-call Connection, surface this as a Phase 66 follow-up -- DO NOT silently absorb. Document in 65-05-SUMMARY.md as a TECH-DEBT entry.

**Step C -- Retire H2 query_conn (final commit):**

Once Step B is green, in a separate atomic commit:
  - Delete src/lib.rs:498-507 entirely (the `let mut query_conn = ptr::null_mut(); duckdb_connect(...); QueryState { catalog: catalog_reader, conn: query_conn };` block).
  - Retire the `QueryState` struct in src/query/table_function.rs:33-37 (or refactor to take a per-call conn at method call). If the struct can be deleted entirely, do so. If some fields (like `catalog`) are still needed by the migrated bind, refactor to a per-call construction at bind time.
  - Verify `cargo check --features extension --no-default-features` passes -- no dangling references to query_conn or QueryState.

**Step D -- Add the concurrent reads regression test:**

Create test/integration/test_concurrent_reads_per_call_conn.py following the test_concurrent_ddl.py threading pattern. The test:
  - Sets up a small DB with 3 semantic views.
  - Spawns 8 Python threads, each opening its own DuckDB connection.
  - Each thread runs `SHOW SEMANTIC DIMENSIONS FROM v1` 10 times in a tight loop.
  - Assert all 80 calls succeed and produce identical row sets.
  - Assert total wall time is reasonable (e.g. < 30 s) -- catches a regression where per-call Connection becomes synchronously serialized due to a shared mutex.

Append the test to whatever runner registers integration tests (if any -- otherwise just ensure it's runnable via `uv run test/integration/test_concurrent_reads_per_call_conn.py`).

**Step E -- Final phase gate:**

Run `just test-all` and `just ci`. Both must be green on milestone/v0.10.0. Any failures must be reconciled before Plan 06.
  </action>
  <verify>
    <automated>just build 2>&amp;1 | tail -10 ; just test-sql 2>&amp;1 | tail -30 ; uv run test/integration/test_adbc_transactions.py 2>&amp;1 | tail -10 ; uv run test/integration/test_multi_db_isolation.py 2>&amp;1 | tail -10 ; uv run test/integration/test_concurrent_reads_per_call_conn.py 2>&amp;1 | tail -20 ; just test-all 2>&amp;1 | tail -15 ; just ci 2>&amp;1 | tail -15</automated>
  </verify>
  <acceptance_criteria>
    - `just build` exits 0
    - `just test-sql` exits 0
    - `just test-all` exits 0
    - `just ci` exits 0
    - `uv run test/integration/test_adbc_transactions.py` exits 0 (D-21 invariant)
    - `uv run test/integration/test_multi_db_isolation.py` exits 0
    - `uv run test/integration/test_concurrent_reads_per_call_conn.py` exits 0
    - `grep -nE 'let mut query_conn' src/lib.rs` returns 0 matches (H2 retired)
    - `grep -nE 'register_table_function_with_extra_info|register_scalar_function_with_state' src/lib.rs` returns 0 matches (all 17 callbacks migrated)
    - QueryState struct deleted from src/query/table_function.rs OR refactored to take per-call conn (verify by reading the struct definition)
    - Phase 64 test surface (qualify_and_quote_table_ref) passes byte-identical
    - H1 catalog_conn at src/lib.rs:386-410 is still present (Plan 06 retires it) -- DO NOT delete it in this plan
  </acceptance_criteria>
  <done>
All 17 read-side callbacks are migrated to the C++ Catalog API. H2 query_conn is retired. just test-all is green. just ci is green. The phase 66 EXPAND-CTX-01 work is unblocked / may be a no-op verification (the root cause likely dissolved when query_conn retired). Plan 06 can now retire H1 and verify the watchdog tests flip green.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| C++ bind callback ↔ Rust FFI dispatcher (17 callbacks) | Crossed at every read-side query; ABI mismatch, panic UB, buffer ownership |
| User SQL → read-side bind callbacks | View names + named args (dimensions / metrics / facts) are untrusted; existing v0.6.0 hardening applies |
| Per-call Connection lifecycle | Opened at bind, dropped at end-of-bind; must not leak across calls; UAF risk if BindData outlives the Connection |
| Process-local type cache | Concurrent reads/writes from multiple sessions; potential data race on the RwLock |
| H2 query_conn retirement | Removing the last long-lived handle that semantic_view depended on; risk of resource leak in the new per-call path |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-65-05-01 | Tampering | FFI buffer ownership (17 callbacks) | mitigate | Every Rust dispatcher uses catch_unwind + sv_free_buffer pattern from Plan 04. C++ side wraps the FFI call in try/catch and re-throws as BinderException. |
| T-65-05-02 | Tampering | Per-call Connection lifetime | mitigate | Connection probe is a stack object; constructor + destructor pair within bind callback scope. The bind's return value (BindData) does NOT carry the Connection -- only the materialized rows (already copied out of the Connection's result). This is the same pattern as Plan 04's __sv_compute_create_from_yaml. |
| T-65-05-03 | Information Disclosure | Type cache cross-session leak | accept | The cache contains only (view name, column types) -- no PII, no permission-bearing data. Multiple sessions querying the same view share the same cached types, which is correct behavior. RWlock prevents data races. |
| T-65-05-04 | Denial of Service | Type cache unbounded growth | accept | Per RESEARCH §6.2: keyspace bounded by `views * distinct_definitions_in_history` per process. Entries are tiny. The Phase 62 LRU was retired precisely to avoid silent-eviction errors (TECH-DEBT 20); unbounded HashMap is the documented alternative. |
| T-65-05-05 | Denial of Service | Per-call Connection construction cost | mitigate | RESEARCH benchmark in 65-READ-PATH-SPIKE.md confirms Connection(*context.db) constructor is sub-millisecond. test_concurrent_reads_per_call_conn.py is the regression guard. |
| T-65-05-06 | Tampering | Phase 64 qualify_and_quote_table_ref regression | mitigate | D-20 invariant. Phase 64 sqllogictest must pass byte-identical after Task 5 and Task 6. Explicit acceptance criterion. |
| T-65-05-07 | Spoofing | Cross-database catalog/schema search-path | mitigate | The per-call Connection inherits the caller's ClientContext, which has the caller's search path. test_multi_db_isolation.py is the regression guard. If a failure surfaces, it is documented as a Phase 66 follow-up (D-22 bounded scope), not silently absorbed. |
| T-65-05-08 | Tampering | Bridge mechanism (Rust↔C++ duckdb_connection wrap) | mitigate | Task 1 spike empirically validates the chosen mechanism. The chosen approach is documented in 65-05-SUMMARY.md with the exact accessor / cast / wrap incantation. |
| T-65-05-09 | Information Disclosure | UNUSED query_conn allocation between Task 5 and Task 6 | accept | After Task 5 + Step B, H2 is allocated but unused. Brief window (between commits within Task 6). Plan 06 still depends on Plan 05 retiring H2 fully. |
| T-65-05-SC | Tampering | npm/pip/cargo installs | mitigate | No new package dependencies in Plan 05. C++ uses existing DuckDB amalgamation; Rust uses existing dependencies. RESEARCH did not flag any [ASSUMED]/[SUS] packages. |
</threat_model>

<verification>
- `just build` exits 0 after each task
- `just test-sql` exits 0 after each task; all 17 migrated read paths produce byte-identical user-visible output
- `cargo test --features extension --no-default-features` exits 0 (includes type_cache::tests)
- `just test-all` exits 0 at end of plan
- `just ci` exits 0 at end of plan
- `uv run test/integration/test_adbc_transactions.py` exits 0 throughout (D-21 invariant)
- `uv run test/integration/test_multi_db_isolation.py` exits 0 (cross-DB resolution works)
- `uv run test/integration/test_concurrent_reads_per_call_conn.py` exits 0 (new concurrent-reads regression)
- `grep -nE 'register_table_function_with_extra_info|register_scalar_function_with_state' src/lib.rs` returns 0 matches at end of plan
- `grep -nE 'let mut query_conn' src/lib.rs` returns 0 matches at end of plan (H2 retired)
- H1 catalog_conn allocation at src/lib.rs:386-410 is STILL PRESENT (Plan 06 retires it)
- Phase 64 qualify_and_quote_table_ref call sites untouched (D-20 invariant)
</verification>

<success_criteria>
- All 17 read-side functions migrated to the C++ Catalog API shim with per-call Connection bind
- Rust↔C++ bridge mechanism documented (Task 1 spike chooses, all 16 other callbacks reuse)
- Process-local type-inference cache (unbounded HashMap, not LRU) introduced; type inference fully deferred from CREATE-time to read-side bind time per D-16/D-17
- H2 query_conn retired (src/lib.rs:498-507 deleted; QueryState struct retired or refactored)
- Phase 64 qualify_and_quote_table_ref wired sites NOT regressed (D-20)
- D-21 transactional invariant green throughout (ADBC tests)
- test_concurrent_reads_per_call_conn.py is the new regression for concurrent per-call connections
- After H2 retirement: just test-all + just ci both green on milestone/v0.10.0
- H1 catalog_conn still present (Plan 06 retires)
- Any cross-database surprise (catalog-search-path divergence persisting) surfaces as TECH-DEBT / Phase 66 follow-up, not silently absorbed (D-22)
</success_criteria>

<output>
Create `.planning/phases/65-overridecontext-connection-teardown/65-05-SUMMARY.md` documenting:
- Wave 0 bridge spike result: the exact Rust↔C++ duckdb_connection wrap mechanism chosen (with code excerpt)
- Per-task migration counts (5 zero-arg + 7 name-arg + 2-arg + 2 scalars + explain + semantic_view = 17 total)
- Type cache module shape + unit test count
- H2 retirement commit SHA + diff stat
- `just test-all` and `just ci` green confirmation
- Cross-database EXPAND-CTX investigation: did the catalog-search-path divergence dissolve when H2 retired? If yes, document for Phase 66 as a finding (no follow-up needed); if no, file as Phase 66 follow-up.
- TECH-DEBT entries surfaced (e.g. any v0.6.0 test that needed adjustment, any subtle behavioral edge case, any [ASSUMED] package legitimacy still pending)
- Forward pointer: "H1 catalog_conn at src/lib.rs:386-410 still present; Plan 06 retires it + adds structural guard test."
</output>
