---
phase: 65-overridecontext-connection-teardown
plan: 04
type: execute
wave: 2
depends_on:
  - 65-03
files_modified:
  - cpp/src/shim.cpp
  - cpp/src/shim.hpp
  - src/parse.rs
  - src/ddl/alter_helpers_ffi.rs
  - src/lib.rs
  - test/sql/65_json_merge_patch_smoke.test
  - test/sql/65_alter_comment_merge_patch.test
  - test/sql/65_alter_rename_via_sql.test
  - test/sql/TEST_LIST
  - test/integration/test_create_from_yaml_v010.py
autonomous: true
requirements:
  - LIFE-02
tags:
  - duckdb
  - rust
  - ffi
  - cpp
  - parser-override
  - alter
  - yaml
  - json_merge_patch
  - catalog-api

must_haves:
  truths:
    - "Wave 0 sqllogictest spike (65_json_merge_patch_smoke.test) confirms DuckDB v1.5.2 `json_merge_patch` implements RFC-7396 null-as-delete semantics"
    - "cpp/src/shim.cpp introduces `sv_register_table_function(db, name, args, bind_cb, exec_cb, init_cb)` from scratch (~150 LOC) following the 65-READ-PATH-SPIKE.md template; A2 resolution"
    - "cpp/src/shim.cpp introduces `__sv_compute_create_from_yaml` table function whose bind callback opens `Connection probe(*context.db)`, reads the YAML file, calls into Rust FFI helper `sv_compute_create_from_yaml_rust`, returns the enriched JSON in a single-row, single-column result of LogicalType::VARCHAR"
    - "src/ddl/alter_helpers_ffi.rs (or equivalent module) defines `#[no_mangle] pub unsafe extern \"C\" fn sv_compute_create_from_yaml_rust(...)` using catch_unwind + sv_free_buffer ownership transfer per src/parse.rs::sv_parser_override_rust pattern"
    - "src/parse.rs::rewrite_alter_comment emits pure-SQL `UPDATE semantic_layer._definitions SET definition = json_merge_patch(definition::JSON, '{\"comment\":\"<escaped>\"}'::JSON)::VARCHAR WHERE name = ?` wrapped in the Phase 60 two-statement race-guard (D-13) for SET COMMENT"
    - "src/parse.rs::rewrite_alter_comment emits `UPDATE ... SET definition = json_merge_patch(definition::JSON, '{\"comment\":null}'::JSON)::VARCHAR ...` for UNSET COMMENT (RFC-7396 null-as-delete; verified by Wave 0 spike)"
    - "src/parse.rs::rewrite_yaml_file_create emits `INSERT INTO _definitions (name, definition) SELECT '<name>', new_def FROM __sv_compute_create_from_yaml(<path>, <name>, <kind>, <comment>) ON CONFLICT (name) DO UPDATE SET definition = excluded.definition RETURNING name AS view_name` -- no Rust-side catalog read"
    - "rewrite_alter_comment no longer calls catalog.lookup() / deserialize / mutate / reserialize -- the JSON mutation runs entirely in DuckDB SQL"
    - "rewrite_yaml_file_create no longer calls `read_text` on the OverrideContext catalog connection -- the read happens inside the helper TF's bind callback on a per-call Connection(*context.db)"
    - "ALTER RENAME TO continues to work via pure-SQL race-guard UPDATE (no JSON touch; Plan 03 already covers this -- Plan 04 only adds the regression test 65_alter_rename_via_sql.test)"
    - "test/integration/test_adbc_transactions.py passes (D-21 transactional invariant) after each commit of this plan"
    - "test/integration/test_create_from_yaml_v010.py passes end-to-end (CREATE FROM YAML FILE via helper TF, plus round-trip through get_ddl())"
    - "ALTER SET TAG / MAKE PRIVATE / MAKE PUBLIC / ADD SYNONYMS / ADD DIMENSION / ADD METRIC / ADD FACT / DROP DIMENSION / DROP METRIC / DROP FACT / DROP RELATIONSHIP / ADD RELATIONSHIP are NOT implemented (locked resolution A7 -- 8 enumerated variants dropped as non-Snowflake-features)"
  artifacts:
    - path: "cpp/src/shim.cpp"
      provides: "sv_register_table_function shim + __sv_compute_create_from_yaml helper TF"
      contains: "sv_register_table_function"
    - path: "src/ddl/alter_helpers_ffi.rs"
      provides: "Rust FFI bridge sv_compute_create_from_yaml_rust"
      contains: "sv_compute_create_from_yaml_rust"
    - path: "test/sql/65_json_merge_patch_smoke.test"
      provides: "Wave 0 RFC-7396 null-as-delete verification"
      contains: "json_merge_patch"
    - path: "test/sql/65_alter_comment_merge_patch.test"
      provides: "ALTER SET/UNSET COMMENT via json_merge_patch regression"
      contains: "ALTER SEMANTIC VIEW"
    - path: "test/integration/test_create_from_yaml_v010.py"
      provides: "End-to-end YAML CREATE through helper-TF path"
      contains: "__sv_compute_create_from_yaml"
  key_links:
    - from: "src/parse.rs::rewrite_alter_comment"
      to: "DuckDB caller's connection"
      via: "UPDATE with json_merge_patch on caller's conn"
      pattern: "json_merge_patch\\(definition"
    - from: "src/parse.rs::rewrite_yaml_file_create"
      to: "__sv_compute_create_from_yaml table function"
      via: "INSERT...SELECT FROM __sv_compute_create_from_yaml(...)"
      pattern: "__sv_compute_create_from_yaml"
    - from: "cpp/src/shim.cpp::sv_create_from_yaml_bind"
      to: "Rust sv_compute_create_from_yaml_rust"
      via: "FFI call from C++ bind callback through Connection(*context.db) bridge"
      pattern: "sv_compute_create_from_yaml_rust"
---

<objective>
Land the ALTER-architecture wave. Wave 0 confirms DuckDB v1.5.2 `json_merge_patch` honors RFC-7396 (null-as-delete) via a 5-minute sqllogictest spike. Wave 1 introduces (a) the `sv_register_table_function` C++ Catalog API shim from scratch (per A2 resolution: NOT in HEAD), (b) the `__sv_compute_create_from_yaml(path, name, kind, comment)` helper table function whose bind callback opens `Connection(*context.db)`, reads the YAML via the per-call connection, calls into Rust to parse+enrich+serialize, and returns the resulting JSON in a single-row result. Wave 2 wires (c) ALTER SET COMMENT / UNSET COMMENT to pure-SQL `json_merge_patch` UPDATE rewrites (eliminating the Plan 03 transitional lookup+mutate+reserialize path), (d) CREATE FROM YAML FILE to the helper-TF subquery pattern. ALTER RENAME TO already rides pure-SQL after Plan 03 -- this plan only adds the regression test.

Per locked resolution A7: only the 3 ALTER variants present in HEAD (RENAME TO, SET COMMENT, UNSET COMMENT) are in scope plus CREATE FROM YAML FILE (D-11). The 8 enumerated future variants (MAKE PRIVATE/PUBLIC, SET TAG, ADD SYNONYMS, ADD/DROP DIMENSION/METRIC/FACT/RELATIONSHIP) are explicitly out of scope -- they do not exist in Snowflake semantic-views and per CLAUDE.md the project tracks Snowflake.

Purpose: Eliminates the OverrideContext CatalogReader's last write-path consumers (Plan 03 left rewrite_alter_comment and rewrite_yaml_file_create still calling catalog.lookup / read_text). After this plan, parser_override emits SQL that never touches a long-lived extension-owned connection.

Output: A C++ Catalog API table-function shim plus one helper TF (`__sv_compute_create_from_yaml`), three pure-SQL ALTER rewrites, and end-to-end test coverage for all three ALTER variants + YAML CREATE.
</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-03-SUMMARY.md

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

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

<!-- Reusable patterns -->
@src/catalog.rs

<!-- Existing tests that must stay green / be extended -->
@test/sql/phase45_alter_comment.test
@test/sql/phase34_1_alter_rename.test
@test/integration/test_readonly_load.py
@test/integration/test_adbc_transactions.py

<interfaces>
<!-- sv_register_table_function shim shape (Task 1) -->

The template from 65-READ-PATH-SPIKE.md lines 21-31 (the verbatim spike registration block) is the starting point. Wrap into a reusable C function with signature:

```cpp
extern "C" bool sv_register_table_function(
    duckdb_database db_handle,
    const char *name,
    const LogicalType *arg_types, size_t arg_count,
    table_function_bind_t bind_cb,
    table_function_t exec_cb,
    table_function_init_local_t init_cb);  // init_cb may be nullptr
```

Internally constructs a `TableFunction`, sets `on_conflict = OnCreateConflict::ALTER_ON_CONFLICT`, and calls `system_catalog.CreateTableFunction(txn, info)`. Header declaration in cpp/src/shim.hpp.

<!-- __sv_compute_create_from_yaml signature (Task 2) -->

`CREATE TABLE FUNCTION __sv_compute_create_from_yaml(file_path VARCHAR, view_name VARCHAR, kind INTEGER, comment VARCHAR) RETURNS TABLE(new_def VARCHAR)`

The bind callback opens `Connection probe(*context.db)`, queries `SELECT content FROM read_text(?)`, then calls `sv_compute_create_from_yaml_rust(...)` with the file content + view_name + kind + comment, receives an owned UTF-8 buffer (ptr+len) containing the enriched JSON, copies into BindData, releases via `sv_free_buffer`, declares one VARCHAR column "new_def", returns the BindData. The exec callback emits a single row with the JSON. `kind` is 0=CREATE, 1=OR REPLACE, 2=IF NOT EXISTS.

<!-- ALTER SET COMMENT / UNSET COMMENT SQL shape (Task 3) -->

```
-- SET COMMENT 'new comment text'
SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM semantic_layer._definitions WHERE name = '<name_escaped>')
            THEN error('semantic view ''<name_escaped>'' was concurrently dropped')
            ELSE TRUE END;
UPDATE semantic_layer._definitions
   SET definition = json_merge_patch(definition::JSON, '<json_object_with_comment_escaped>'::JSON)::VARCHAR
 WHERE name = '<name_escaped>'
RETURNING name AS view_name;

-- UNSET COMMENT (RFC-7396 null-as-delete -- Wave 0 verifies)
SELECT CASE WHEN NOT EXISTS (...) THEN error(...) ELSE TRUE END;
UPDATE semantic_layer._definitions
   SET definition = json_merge_patch(definition::JSON, '{"comment":null}'::JSON)::VARCHAR
 WHERE name = '<name_escaped>'
RETURNING name AS view_name;
```

The JSON literal for SET COMMENT is built via `serde_json::to_string(&serde_json::json!({"comment": user_comment}))` so internal quote escaping is correct; then `escape_sql_arg` doubles single quotes for the outer SQL literal.

<!-- CREATE FROM YAML FILE SQL shape (Task 4) -->

```
-- Plain CREATE
INSERT INTO semantic_layer._definitions (name, definition)
SELECT
  CASE WHEN EXISTS (SELECT 1 FROM semantic_layer._definitions WHERE name = '<name_escaped>')
       THEN error('semantic view ''<name_escaped>'' already exists; use CREATE OR REPLACE SEMANTIC VIEW to overwrite')
       ELSE '<name_escaped>' END,
  new_def
FROM __sv_compute_create_from_yaml('<path_escaped>', '<name_escaped>', 0, '<comment_escaped>')
RETURNING name AS view_name;

-- OR REPLACE
INSERT INTO semantic_layer._definitions (name, definition)
SELECT '<name_escaped>', new_def
FROM __sv_compute_create_from_yaml('<path_escaped>', '<name_escaped>', 1, '<comment_escaped>')
ON CONFLICT (name) DO UPDATE SET definition = excluded.definition
RETURNING name AS view_name;

-- IF NOT EXISTS
INSERT OR IGNORE INTO semantic_layer._definitions (name, definition)
SELECT '<name_escaped>', new_def
FROM __sv_compute_create_from_yaml('<path_escaped>', '<name_escaped>', 2, '<comment_escaped>')
RETURNING name AS view_name;
```

<!-- Out of scope per A7 -->

NOT implemented in this plan (Snowflake non-features per CLAUDE.md "If in doubt about SQL syntax or behaviour refer to what Snowflake semantic views does."):
- ALTER MAKE PRIVATE / MAKE PUBLIC
- ALTER SET TAG
- ALTER ADD SYNONYMS
- ALTER ADD DIMENSION / ADD METRIC / ADD FACT / ADD RELATIONSHIP
- ALTER DROP DIMENSION / DROP METRIC / DROP FACT / DROP RELATIONSHIP

These are dropped as non-features, NOT deferred. Do not add code paths or test scaffolding for them.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1 (Wave 0): json_merge_patch RFC-7396 null-as-delete spike</name>
  <files>test/sql/65_json_merge_patch_smoke.test, test/sql/TEST_LIST</files>
  <read_first>
    - 65-CONTEXT.md "Resolutions during plan-phase" section A1
    - 65-RESEARCH.md §4.1 "Open empirical question for Plan 04 (Wave-0 spike)"
    - 65-PATTERNS.md "Analog for `test/sql/65_json_merge_patch_smoke.test`" (lines 416-425)
    - test/sql/peg_compat.test (single-statement test shape template)
    - DuckDB JSON docs (already confirmed: `json_merge_patch` exists; full RFC-7396 behavior unverified at research time)
  </read_first>
  <behavior>
    - Test 1: `SELECT json_merge_patch('{"a":1,"b":2}'::JSON, '{"b":null}'::JSON)` returns `{"a":1}` (RFC-7396 null-as-delete confirmed).
    - Test 2: `SELECT json_merge_patch('{"comment":"old"}'::JSON, '{"comment":"new"}'::JSON)` returns `{"comment":"new"}` (overwrite case -- foundation for SET COMMENT).
    - Test 3: `SELECT json_merge_patch('{"comment":"old","other":42}'::JSON, '{"comment":null}'::JSON)` returns `{"other":42}` (null-as-delete on a real shape).
    - Test 4: `SELECT json_merge_patch('{}'::JSON, '{"a":null}'::JSON)` returns `{}` (null in patch on absent key is a no-op per RFC-7396).
  </behavior>
  <action>
Create test/sql/65_json_merge_patch_smoke.test using the single-statement sqllogictest shape from test/sql/peg_compat.test (start with `require semantic_views` directive). Add the 4 query assertions above using `query T` blocks with `----` separators (the exact JSON-string-output format per existing sqllogictest convention). Cast outputs to JSON and back via `::VARCHAR` to canonicalize whitespace if needed -- match the exact output format of `json_merge_patch` on DuckDB v1.5.2 (run interactively first to capture the canonical output, then write the assertion).

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

If ANY of the 4 assertions fail at runtime, halt the plan immediately. Per A1 resolution this is a 5-minute gate: if `json_merge_patch` does not honor RFC-7396 in DuckDB v1.5.2, the UNSET COMMENT mechanism must redesign (likely to a regenerate-via-helper-TF path) before Task 3 runs. Report the failure to the user via the executor summary and request replanning.

Do NOT proceed to Task 2 until this task is green.
  </action>
  <verify>
    <automated>just test-sql test/sql/65_json_merge_patch_smoke.test 2>&amp;1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - `just test-sql test/sql/65_json_merge_patch_smoke.test` exits 0
    - All 4 query assertions pass with the exact expected outputs
    - test/sql/TEST_LIST contains a line referencing `65_json_merge_patch_smoke.test`
    - If any assertion fails: executor halts the plan, surfaces the failure clearly, and requests user replan (per A1's "Plan 04's first task must be this spike (~5 min); planner halts and replans if it fails")
  </acceptance_criteria>
  <done>
RFC-7396 null-as-delete is confirmed in DuckDB v1.5.2 `json_merge_patch`. The mechanism for UNSET COMMENT in Task 3 is unblocked. If the spike fails, the plan halts pending user direction.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2 (Wave 1): Introduce sv_register_table_function C++ shim and __sv_compute_create_from_yaml helper TF</name>
  <files>cpp/src/shim.cpp, cpp/src/shim.hpp, src/ddl/alter_helpers_ffi.rs, src/lib.rs, src/ddl/mod.rs</files>
  <read_first>
    - 65-CONTEXT.md `<decisions>` D-04, D-11, D-12; "Resolutions during plan-phase" section A2
    - 65-RESEARCH.md §1.2 (entire section, especially the C++ bind sketch lines 177-205) + §5.1 (A2 finding) + §5.2 idiomatic shape + §5.3 community-extension patterns
    - 65-PATTERNS.md "Plan 04" section starting at line 326 + "Connection-from-bind" shared pattern at line 634-651
    - 65-READ-PATH-SPIKE.md (entire file -- this is the template for sv_register_table_function and for the bind+exec+init triple)
    - 65-ALTER-REWRITE-SPIKE.md (UPDATE-with-TF-subquery pattern proves the outer shape binds on DuckDB v1.5.2)
    - cpp/src/shim.cpp (current state; line 407 length -- adding ~200 LOC per RESEARCH §5.4 recommendation keeps in one TU)
    - cpp/src/shim.hpp (current declarations)
    - src/parse.rs::sv_parser_override_rust (FFI ptr+len + catch_unwind pattern -- existing analog for the Rust FFI helper)
    - src/parse.rs (sv_free_buffer function and SvOwnedBuffer semantics on the C++ side; PATTERNS.md "FFI ptr+len + sv_free_buffer" shared pattern at line 667-674)
    - src/model.rs (`SemanticViewDefinition::from_yaml_with_size_cap`; YAML_SIZE_CAP = 1 MiB)
    - src/ddl/define.rs (slimmed `enrich_definition_for_create` from Plan 03 -- this is what the Rust FFI helper calls into)
    - Context7 docs for DuckDB v1.5 C++ TableFunction registration if any aspect of the API is unclear (`ctx7 library duckdb` then query "table function registration system_catalog")
  </read_first>
  <behavior>
    - Test 1 (link gate): `just build` succeeds after adding the C++ + Rust FFI surfaces; no undefined symbols.
    - Test 2 (registration): After extension load, `SELECT function_name FROM duckdb_functions() WHERE function_name = '__sv_compute_create_from_yaml'` returns one row.
    - Test 3 (bind call): `SELECT * FROM __sv_compute_create_from_yaml('test/integration/fixtures/example_view.yaml', 'v', 0, '')` returns one row whose `new_def` is a non-empty VARCHAR matching a JSON object containing the keys `name`, `tables`, `dimensions`, `metrics`, `created_on`, `database_name`, `schema_name` (the same shape the parser_override INSERT produces).
    - Test 4 (per-call connection): Two parallel sessions calling the TF on the same path produce identical `new_def` content (no shared mutable state between bind invocations).
    - Test 5 (security): `SELECT * FROM __sv_compute_create_from_yaml('/etc/passwd', 'evil', 0, '')` returns a parse error from inside the YAML parser (file is non-YAML), NOT a path-traversal escape. Verify the YAML_SIZE_CAP applies (a 2 MiB file is rejected with a "size cap exceeded" error).
    - Test 6 (panic safety): A Rust panic inside the FFI helper does not unwind across the C++ boundary -- it returns a non-zero error code and a stringified error message via the error_buf (per catch_unwind pattern).
  </behavior>
  <action>
**Step A -- Introduce sv_register_table_function in cpp/src/shim.cpp (per A2 resolution, from scratch):**

Add at the bottom of cpp/src/shim.cpp (or just before the existing extern "C" block end), a reusable C-callable registration function using the template from 65-READ-PATH-SPIKE.md lines 21-31. Signature per the `<interfaces>` block. Inside the body: build a `TableFunction tf(name, arg_types_vec, exec_cb, bind_cb, init_cb)`, set `info.on_conflict = OnCreateConflict::ALTER_ON_CONFLICT`, retrieve `&system_catalog = Catalog::GetSystemCatalog(db)`, get `txn = CatalogTransaction::GetSystemTransaction(db)`, call `system_catalog.CreateTableFunction(txn, info)`. Wrap in try/catch -- any thrown exception returns false; success returns true.

Add the declaration to cpp/src/shim.hpp inside the `extern "C"` block.

**Step B -- Introduce __sv_compute_create_from_yaml helper TF in cpp/src/shim.cpp:**

Define `struct CreateFromYamlBindData : TableFunctionData { string file_path, view_name, comment, new_def; int kind; bool row_emitted; }` (the `row_emitted` flag lets the exec callback emit exactly one row then return).

Implement `static unique_ptr<FunctionData> sv_create_from_yaml_bind(ClientContext &context, TableFunctionBindInput &input, vector<LogicalType> &return_types, vector<string> &names)` per RESEARCH §1.2 lines 187-204:
  - Extract `file_path`, `view_name`, `kind`, `comment` from `input.inputs[0..3]`.
  - Open `Connection probe(*context.db)`.
  - Call `probe.Query("SELECT content FROM read_text(?)")` with the file_path as a parameterized arg (use `probe.Prepare(...).Execute(...)` with `Value`-bound args to avoid SQL injection on the path).
  - Validate the content size against YAML_SIZE_CAP (1 MiB) -- if exceeded, throw a `BinderException("YAML file size exceeds 1 MiB cap")`.
  - Call `sv_compute_create_from_yaml_rust(content_ptr, content_len, view_name_ptr, view_name_len, comment_ptr, comment_len, kind, &out_ptr, &out_len, error_buf, error_buf_len)` -- the Rust FFI helper introduced in Step C.
  - If the Rust helper returns non-zero, throw a `BinderException(error_buf)`.
  - Copy out_ptr/out_len into `bd->new_def`, then `sv_free_buffer(out_ptr, out_len)` to release.
  - `return_types.push_back(LogicalType::VARCHAR); names.push_back("new_def");`
  - Return `std::move(bd)`.

Implement `static void sv_create_from_yaml_function(ClientContext &context, TableFunctionInput &data_p, DataChunk &output)`:
  - Cast `data_p.bind_data` to `CreateFromYamlBindData *`.
  - If `bd->row_emitted`, set `output.SetCardinality(0)` and return.
  - Otherwise, set the single VARCHAR cell of output to bd->new_def via `output.SetValue(0, 0, Value(bd->new_def))`, `output.SetCardinality(1)`, set `bd->row_emitted = true`.

In `sv_register_parser_hooks` (or a sibling new initializer called from init_extension), call `sv_register_table_function(db_handle, "__sv_compute_create_from_yaml", {LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::INTEGER, LogicalType::VARCHAR}, 4, sv_create_from_yaml_bind, sv_create_from_yaml_function, nullptr)`. Confirm the registration via Test 2.

**Step C -- Rust FFI helper `sv_compute_create_from_yaml_rust` (new module src/ddl/alter_helpers_ffi.rs):**

Create src/ddl/alter_helpers_ffi.rs with:

```rust
#[cfg(feature = "extension")]
#[no_mangle]
pub unsafe extern "C" fn sv_compute_create_from_yaml_rust(
    content_ptr: *const u8, content_len: usize,
    name_ptr: *const u8, name_len: usize,
    comment_ptr: *const u8, comment_len: usize,
    kind: u8,
    out_ptr: *mut *mut u8,
    out_len: *mut usize,
    error_buf: *mut u8,
    error_buf_len: usize,
) -> u8
```

Inside, use `std::panic::catch_unwind(AssertUnwindSafe(|| { ... }))` per src/parse.rs::sv_parser_override_rust at lines ~2620+. Body steps:
  1. Read content slice from (ptr, len). Same for name, comment.
  2. Parse YAML via `crate::model::SemanticViewDefinition::from_yaml_with_size_cap(content, YAML_SIZE_CAP)`.
  3. Apply view-level comment if `comment_len > 0` (set `def.comment = Some(comment_str.to_owned())`).
  4. Call the slimmed `crate::ddl::define::enrich_definition_for_create(name, def)` (from Plan 03 -- no `conn` param) to get the JSON string. Note: this function path does NOT capture metadata (Plan 03 left that to the SQL caller). For the YAML helper, metadata still gets added by DuckDB on the caller's conn via an outer json_merge_patch in the INSERT shape -- OR you can choose to keep metadata-via-SQL only at the outer INSERT and have the helper TF return a metadata-less JSON. Choose the latter: helper TF returns metadata-less JSON; the outer INSERT in Task 4 wraps it via json_merge_patch with `now()` / `current_database()` / `current_schema()` to preserve behavioral parity with Plan 03's non-YAML CREATE path. Document this choice in the SUMMARY.
  5. Allocate a `Box<[u8]>` holding the JSON bytes, leak via `Box::into_raw(...) as *mut u8`, write the pointer and length to `*out_ptr` / `*out_len`. Caller must release via `sv_free_buffer(out_ptr, out_len)`.
  6. Return 0 on success, 1 on YAML parse error, 2 on enrichment error, etc. On error: write the formatted error message into `error_buf` up to `error_buf_len - 1` bytes with NUL terminator.

Declare the module in src/ddl/mod.rs (`pub mod alter_helpers_ffi;`).

Reuse the existing `sv_free_buffer` (it does not care whether the buffer was allocated by the parser_override path or the YAML helper -- both are heap-owned Box<[u8]> the same way).

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

Run `just build`. Then run the manual probes from Test 2-5 via a transient sqllogictest in test/sql/65_yaml_helper_smoke.test (do NOT add to TEST_LIST; this is for confidence during Task 2 only). Once green, delete the file -- Task 4 will add the production tests.
  </action>
  <verify>
    <automated>just build 2>&amp;1 | tail -10 ; cargo test --features extension --no-default-features alter_helpers_ffi 2>&amp;1 | tail -10 ; just test-sql test/sql/65_json_merge_patch_smoke.test 2>&amp;1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - `just build` exits 0; no undefined symbols
    - `grep -c 'sv_register_table_function' cpp/src/shim.cpp` returns at least 2 (definition + at least one call site -- Step B's registration of the YAML helper TF)
    - `grep -c 'sv_register_table_function' cpp/src/shim.hpp` returns at least 1 (declaration)
    - `grep -c 'sv_compute_create_from_yaml_rust' src/ddl/alter_helpers_ffi.rs` returns at least 1
    - `grep -c 'catch_unwind' src/ddl/alter_helpers_ffi.rs` returns at least 1
    - A short manual probe (a one-off sqllogictest or `duckdb` CLI run) of `SELECT new_def FROM __sv_compute_create_from_yaml(<path>, 'v', 0, '')` against a valid YAML fixture returns a single VARCHAR row containing a JSON object with `name`, `tables`, `dimensions`, `metrics` keys
    - Panic across FFI is caught: a deliberately malformed YAML returns rc != 0 and an error_buf containing a useful message
    - File size cap is enforced: a YAML > 1 MiB returns rc != 0 with "size cap exceeded" or similar
  </acceptance_criteria>
  <done>
The C++ Catalog API shim infrastructure is in place. The YAML helper TF is registered and callable. The Rust FFI bridge follows the catch_unwind + sv_free_buffer convention. Tasks 3 and 4 can now build on top.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3 (Wave 2): Wire ALTER SET/UNSET COMMENT to pure-SQL json_merge_patch UPDATE; add 65_alter_comment_merge_patch.test and 65_alter_rename_via_sql.test</name>
  <files>src/parse.rs, test/sql/65_alter_comment_merge_patch.test, test/sql/65_alter_rename_via_sql.test, test/sql/TEST_LIST</files>
  <read_first>
    - src/parse.rs::rewrite_alter_comment (lines 2314-2383 post-Plan-03 -- currently the v0.9.0 lookup+mutate+reserialize shape)
    - src/parse.rs::race_guard_select (lines 2190-2197) -- D-13 unchanged
    - src/parse.rs::escape_sql_arg (line ~2170-2172) -- SQL literal escaping
    - 65-CONTEXT.md `<decisions>` D-09 (corrected by A1 to json_merge_patch), D-13 (race-guard unchanged); "Resolutions during plan-phase" section A1, A7
    - 65-RESEARCH.md §4.1 "Current state" table for the 3 ALTER variants, and the corrected SQL shapes
    - 65-PATTERNS.md "src/parse.rs::rewrite_alter_comment" section (lines 162-196) + "JSON-specific" security note at line 686
    - test/sql/phase45_alter_comment.test (existing ALT-01 / ALT-02 cases -- must continue to pass byte-identical at result level)
    - test/sql/phase34_1_alter_rename.test (existing ALTER RENAME tests -- must continue to pass)
  </read_first>
  <behavior>
    - Test 1: `ALTER SEMANTIC VIEW v SET COMMENT 'new comment'` updates the persisted JSON's `comment` field to "new comment". `SELECT comment FROM list_semantic_views() WHERE name = 'v'` returns 'new comment'.
    - Test 2: `ALTER SEMANTIC VIEW v UNSET COMMENT` removes the `comment` key from the persisted JSON. `SELECT comment FROM list_semantic_views() WHERE name = 'v'` returns NULL.
    - Test 3: After ALTER SET COMMENT on a non-existent view, the emitted SQL fails with the race-guard error message ("semantic view 'v' was concurrently dropped").
    - Test 4: ALTER SET COMMENT with a comment containing `'`, `"`, and `\` characters round-trips correctly -- the JSON escaping via `serde_json::to_string` and the SQL escaping via `escape_sql_arg` produce byte-identical content on read.
    - Test 5: ALTER RENAME TO continues to work (Plan 03 already covers this; Task 3 adds the regression test 65_alter_rename_via_sql.test).
    - Test 6: phase45_alter_comment.test passes byte-identical (no edits needed -- user-visible behavior unchanged).
    - Test 7: Cross-database ALTER SET COMMENT (ATTACH 'b.db' AS db2; ALTER SEMANTIC VIEW db2.main.v SET COMMENT 'x') updates the JSON in the right `_definitions` table (caller's connection drives the resolution).
  </behavior>
  <action>
**Replace src/parse.rs::rewrite_alter_comment body** (lines 2314-2383) per PATTERNS.md lines 162-196 + the `<interfaces>` shape block above. Specifically:

For SET COMMENT:
  - Take the comment value as the user-supplied string.
  - Build the JSON patch via `let patch = serde_json::to_string(&serde_json::json!({"comment": comment_str})).map_err(|e| ParseError { message: format!("failed to build comment patch: {e}"), position: None })?;`
  - Apply SQL escaping via `let patch_escaped = escape_sql_arg(&patch);`
  - Emit:
    ```
    {race_guard_select};
    UPDATE semantic_layer._definitions
       SET definition = json_merge_patch(definition::JSON, '{patch_escaped}'::JSON)::VARCHAR
     WHERE name = '{name_escaped}'
    RETURNING name AS view_name
    ```
  - Wrap in `Ok(Some(format!(...)))` matching the existing rewrite_alter_comment return shape.

For UNSET COMMENT:
  - Same shape but with the patch literal `'{"comment":null}'::JSON` (RFC-7396 null-as-delete, verified by Task 1).
  - No serde_json invocation needed -- the patch literal is constant.

**Delete the lookup+mutate+reserialize block** entirely. The function no longer touches `catalog.lookup()` for SET/UNSET COMMENT. (rewrite_yaml_file_create still uses CatalogReader after Plan 03; Task 4 retires that.)

**Verify the existing ALT-01 / ALT-02 sqllogictests** in test/sql/phase45_alter_comment.test continue to pass byte-identical at the result level. If they don't, the emitted SQL diverges in a user-visible way -- reconcile by adjusting the SQL shape to match phase45 expectations (e.g. ordering of RETURNING columns).

**Create test/sql/65_alter_comment_merge_patch.test** with assertions covering Tests 1-4 + 7 above. Use the standard sqllogictest shape (require semantic_views; CREATE TABLE; CREATE SEMANTIC VIEW; ALTER ... SET/UNSET COMMENT; query list_semantic_views or describe_semantic_view; assert). Include the cross-database (ATTACH) case per Test 7.

**Create test/sql/65_alter_rename_via_sql.test** with assertions:
  - `ALTER SEMANTIC VIEW v RENAME TO w` succeeds and `SELECT name FROM list_semantic_views() WHERE name = 'w'` returns 1 row.
  - `ALTER SEMANTIC VIEW does_not_exist RENAME TO whatever` fails with the race-guard "concurrently dropped" error.
  - `ALTER SEMANTIC VIEW v RENAME TO v_existing` where `v_existing` already exists fails with the "already exists" error (the dual-pre-check pattern from Plan 03).

Append `65_alter_comment_merge_patch.test` and `65_alter_rename_via_sql.test` to test/sql/TEST_LIST.
  </action>
  <verify>
    <automated>just test-sql 2>&amp;1 | tail -20 ; just test-sql test/sql/phase45_alter_comment.test test/sql/phase34_1_alter_rename.test test/sql/65_alter_comment_merge_patch.test test/sql/65_alter_rename_via_sql.test 2>&amp;1 | tail -10 ; uv run test/integration/test_adbc_transactions.py 2>&amp;1 | tail -10 ; cargo test --lib --features extension --no-default-features 2>&amp;1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `just test-sql test/sql/65_alter_comment_merge_patch.test` exits 0 with all assertions passing
    - `just test-sql test/sql/65_alter_rename_via_sql.test` exits 0 with all assertions passing
    - `just test-sql test/sql/phase45_alter_comment.test` exits 0 byte-identical (no edits to phase45 needed)
    - `just test-sql test/sql/phase34_1_alter_rename.test` exits 0
    - `uv run test/integration/test_adbc_transactions.py` exits 0 (D-21 invariant)
    - `grep -c 'json_merge_patch' src/parse.rs` returns at least 1 (rewrite_alter_comment now emits json_merge_patch)
    - `grep -c 'catalog\.lookup' src/parse.rs` returns 0 inside the body of rewrite_alter_comment (use line-range grep). rewrite_yaml_file_create may still contain a catalog.lookup call until Task 4 retires it.
  </acceptance_criteria>
  <done>
ALTER SET COMMENT / UNSET COMMENT now run as pure-SQL `json_merge_patch` UPDATE rewrites on the caller's connection -- no catalog read, no Rust-side JSON mutation. ALTER RENAME continues to work and has its own regression coverage. The user-visible behavior is identical to v0.9.0 byte-for-byte.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 4 (Wave 2): Wire CREATE FROM YAML FILE to __sv_compute_create_from_yaml helper-TF subquery; add test_create_from_yaml_v010.py</name>
  <files>src/parse.rs, test/integration/test_create_from_yaml_v010.py</files>
  <read_first>
    - src/parse.rs::rewrite_yaml_file_create (lines 2051-2148 post-Plan-03 -- currently the v0.9.0 CatalogReader+read_text shape per RESEARCH §1.1 Task 7 option (a))
    - 65-CONTEXT.md `<decisions>` D-11, D-12; D-21 (transactional invariant)
    - 65-RESEARCH.md §1.2 "Outer parser_override emits" block (lines 207-217) + §11 V12 Files security row
    - 65-PATTERNS.md "src/parse.rs::rewrite_yaml_file_create" section (lines 389-392) + "Outer INSERT shape" block (lines 379-388)
    - test/integration/test_readonly_load.py (PEP-723 header + tempdir + open_writable + _connect_with_watchdog pattern for the integration test scaffolding)
    - test/sql/phase53_yaml_file_loading.test (existing CREATE FROM YAML FILE tests -- must continue to pass byte-identical at result level)
    - The helper TF introduced in Task 2: `__sv_compute_create_from_yaml(file_path, view_name, kind, comment)`
  </read_first>
  <behavior>
    - Test 1: `CREATE SEMANTIC VIEW v FROM YAML FILE '/path/to/view.yaml'` populates semantic_layer._definitions with a row whose JSON contains all the YAML's content + populated metadata (created_on / database_name / schema_name from the caller's connection).
    - Test 2: `CREATE OR REPLACE SEMANTIC VIEW v FROM YAML FILE '/path/to/view2.yaml'` replaces an existing definition (ON CONFLICT UPDATE).
    - Test 3: `CREATE SEMANTIC VIEW IF NOT EXISTS v FROM YAML FILE '/path/to/view.yaml'` silently does nothing if `v` already exists (INSERT OR IGNORE).
    - Test 4: `CREATE SEMANTIC VIEW v FROM YAML FILE '/path/to/nonexistent.yaml'` fails with a DuckDB IO error from `read_text` inside the helper TF bind.
    - Test 5: `CREATE SEMANTIC VIEW v FROM YAML FILE '/path/to/malformed.yaml'` fails with a YAML parse error from the Rust FFI helper.
    - Test 6: A 2 MiB YAML file is rejected with "YAML file size exceeds 1 MiB cap" (YAML_SIZE_CAP).
    - Test 7: Inside a user transaction (`BEGIN; CREATE SEMANTIC VIEW v FROM YAML FILE '...'; ROLLBACK;`), the CREATE participates in the transaction -- after ROLLBACK, the view does NOT exist (D-21 transactional invariant).
    - Test 8: get_ddl('v') after CREATE FROM YAML FILE returns DDL that round-trips: re-parsing the DDL produces an equivalent view.
    - Test 9: phase53_yaml_file_loading.test continues to pass byte-identical (existing user-visible behavior unchanged).
  </behavior>
  <action>
**Replace src/parse.rs::rewrite_yaml_file_create body** (lines 2051-2148):
  - Keep the sentinel-parsing prologue at lines 2062-2087 unchanged (CREATE / OR REPLACE / IF NOT EXISTS / comment extraction).
  - DELETE the `catalog.read_text(path)` and `SemanticViewDefinition::from_yaml_with_size_cap` calls + the cardinality inference + serialization at lines 2089-2146. All of that now happens inside the helper TF's bind callback (Task 2 Step C).
  - Emit one of the three SQL shapes per the `<interfaces>` block above (plain / OR REPLACE / IF NOT EXISTS). Path, name, and comment go through `escape_sql_arg`; `kind` is 0/1/2 inline.

Wrap the outer INSERT in the metadata-via-SQL shape from Plan 03 -- since the helper TF returns a metadata-less JSON (Task 2 Step C decision), the outer INSERT applies `json_merge_patch(new_def::JSON, json_object('created_on', strftime(now(), '%Y-%m-%dT%H:%M:%SZ'), 'database_name', current_database(), 'schema_name', current_schema()))::VARCHAR` to add metadata in the caller's connection context. This keeps YAML CREATE byte-identical with non-YAML CREATE for the metadata fields.

Actual emit shape:
```
INSERT INTO semantic_layer._definitions (name, definition)
SELECT
  CASE WHEN EXISTS (SELECT 1 FROM semantic_layer._definitions WHERE name = '<name_escaped>')
       THEN error('semantic view ''<name_escaped>'' already exists; use CREATE OR REPLACE SEMANTIC VIEW to overwrite')
       ELSE '<name_escaped>' END,
  json_merge_patch(
    new_def::JSON,
    json_object(
      'created_on', strftime(now(), '%Y-%m-%dT%H:%M:%SZ'),
      'database_name', current_database(),
      'schema_name', current_schema()
    )
  )::VARCHAR
FROM __sv_compute_create_from_yaml('<path_escaped>', '<name_escaped>', 0, '<comment_escaped>')
RETURNING name AS view_name
```

For OR REPLACE: drop the CASE+EXISTS pre-check and add `ON CONFLICT (name) DO UPDATE SET definition = excluded.definition`.

For IF NOT EXISTS: use `INSERT OR IGNORE INTO ...`.

**Create test/integration/test_create_from_yaml_v010.py** using the PEP-723 header + tempdir + open_writable pattern from test/integration/test_readonly_load.py. Include:
  - A small in-test YAML fixture (write to tempdir) representing a valid semantic view (~30 lines: a TABLES section + DIMENSIONS + METRICS).
  - Test 1-3: CREATE / OR REPLACE / IF NOT EXISTS each succeed and produce expected DB state.
  - Test 4: nonexistent path → IO error.
  - Test 5: malformed YAML → parse error.
  - Test 6: 2 MiB YAML → size-cap error.
  - Test 7: BEGIN/CREATE/ROLLBACK transactional test -- after rollback, view does not exist.
  - Test 8: get_ddl round-trip.

**Verify phase53_yaml_file_loading.test** passes byte-identical. If it fails, the SQL shape diverges in a user-visible way -- reconcile.

After this task, the OverrideContext CatalogReader has zero remaining consumers under parser_override (rewrite_alter_comment was migrated in Task 3; rewrite_yaml_file_create is migrated here; rewrite_drop and rewrite_alter_rename never needed it under D-13 race-guard pattern). H1 catalog_conn allocation in init_extension is STILL PRESENT but is now truly unused -- Plan 06 retires the allocation.
  </action>
  <verify>
    <automated>just test-sql 2>&amp;1 | tail -20 ; just test-sql test/sql/phase53_yaml_file_loading.test 2>&amp;1 | tail -10 ; uv run test/integration/test_create_from_yaml_v010.py 2>&amp;1 | tail -20 ; uv run test/integration/test_adbc_transactions.py 2>&amp;1 | tail -10 ; just test-all 2>&amp;1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `just test-sql test/sql/phase53_yaml_file_loading.test` exits 0 byte-identical (no edits to phase53 needed)
    - `uv run test/integration/test_create_from_yaml_v010.py` exits 0 with all 8 tests passing
    - `uv run test/integration/test_adbc_transactions.py` exits 0 (D-21 invariant)
    - `just test-all` exits 0
    - `grep -c '__sv_compute_create_from_yaml' src/parse.rs` returns at least 1 (rewrite_yaml_file_create emits the helper-TF subquery)
    - `grep -nE 'catalog\.read_text|from_yaml_with_size_cap' src/parse.rs` returns 0 matches (YAML parsing moved out of parse.rs)
    - rewrite_yaml_file_create no longer takes a `catalog: &CatalogReader` argument (or no longer uses the OverrideContext's catalog -- check the function signature)
    - The transactional test (Test 7) confirms ROLLBACK undoes the YAML-based CREATE -- D-21 invariant holds for the helper-TF path
  </acceptance_criteria>
  <done>
CREATE FROM YAML FILE rides the `__sv_compute_create_from_yaml` helper-TF subquery pattern. parser_override has zero remaining catalog read consumers. Plan 05 can now safely retire the H2 query_conn (and Plan 06 can retire H1 catalog_conn).
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| User SQL → parser_override ALTER/CREATE FROM YAML rewrite | Untrusted comment values, view names, file paths reach our emitted SQL; SQL injection / JSON injection |
| C++ bind callback ↔ Rust FFI helper (sv_compute_create_from_yaml_rust) | Crossed on every CREATE FROM YAML FILE; ABI mismatch, panic UB, buffer ownership transfer |
| Rust YAML parsing | Untrusted file content (size, structure, anchor bombs); existing v0.7.0 hardening via from_yaml_with_size_cap |
| `read_text` inside helper TF bind | File-system read; subject to DuckDB's `enable_external_access` settings |
| Caller's connection ↔ semantic_layer._definitions writes via helper-TF subquery | Transactional contract -- the UPDATE/INSERT must participate in caller's BEGIN/COMMIT (D-21) |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-65-04-01 | Tampering | json_merge_patch SQL literal in rewrite_alter_comment | mitigate | Build the JSON patch via `serde_json::to_string(&serde_json::json!({"comment": user_comment}))` so internal `"` and `\` are JSON-escaped. Then `escape_sql_arg` doubles single quotes for the outer SQL literal. Never concatenate raw user input. |
| T-65-04-02 | Tampering | File path arg to __sv_compute_create_from_yaml | mitigate | Path goes through `escape_sql_arg` in src/parse.rs before being embedded in the SELECT FROM clause. Inside the helper TF, the bind callback uses `Value`-bound parameterized query for `read_text(?)` so the path cannot break out of its slot. |
| T-65-04-03 | Tampering | FFI buffer ownership transfer (out_ptr/out_len) | mitigate | Follow the existing src/parse.rs::sv_parser_override_rust + sv_free_buffer convention: Rust allocates with `Box<[u8]>::into_raw`, C++ wraps in SvOwnedBuffer (RAII) so the buffer releases even if downstream throws, release is the exact (ptr, len) Rust returned. |
| T-65-04-04 | Denial of Service | YAML anchor bomb / size attack | mitigate | YAML_SIZE_CAP (1 MiB) enforced at the bind callback's read_text validation (size check) AND inside the Rust FFI helper via from_yaml_with_size_cap. Two-layer defense. |
| T-65-04-05 | Information Disclosure | Path traversal in CREATE FROM YAML FILE | mitigate | The bind callback delegates to DuckDB's `read_text(?)` which honors `enable_external_access` and other DuckDB-level safety settings. No new path-resolution surface introduced. |
| T-65-04-06 | Tampering | UPDATE shape in rewrite_alter_comment must run on caller's connection (D-21) | mitigate | parser_override returns `vector<unique_ptr<SQLStatement>>` from `Parser::ParseQuery(native_sql)`. This is the only DuckDB v1.5.2 mechanism that delivers transactional DDL (verified by EXEC-TIME-SPIKE.md). Test 7 of Task 4 (BEGIN/CREATE/ROLLBACK) is the regression guard. |
| T-65-04-07 | Tampering | FFI panic crossing the Rust/C++ boundary | mitigate | Every `#[no_mangle] extern "C"` Rust function wraps body in `std::panic::catch_unwind(AssertUnwindSafe(...))`. Returns non-zero rc + error_buf on panic. C++ side wraps the FFI call in try/catch and re-throws as BinderException. |
| T-65-04-08 | Spoofing | UNSET COMMENT null-as-delete depends on RFC-7396 behavior | mitigate | Task 1 (Wave 0) sqllogictest spike empirically verifies the behavior in DuckDB v1.5.2 before Task 3 builds on the assumption. Plan halts on spike failure. |
| T-65-04-SC | Tampering | npm/pip/cargo installs | mitigate | No new package dependencies in Plan 04. C++ side uses existing DuckDB amalgamation. Rust side uses already-included serde_json and libduckdb-sys. PEP-723 integration test uses `duckdb==1.5.2` which already pins to the project's tested DuckDB version per existing test_readonly_load.py convention. RESEARCH did not flag any [ASSUMED]/[SUS] packages. |
</threat_model>

<verification>
- `just test-sql` exits 0 with at least 52 tests passing (49 from Plan 03 + 65_json_merge_patch_smoke + 65_alter_comment_merge_patch + 65_alter_rename_via_sql)
- `just test-sql test/sql/phase53_yaml_file_loading.test` exits 0 byte-identical
- `just test-sql test/sql/phase45_alter_comment.test` exits 0 byte-identical
- `uv run test/integration/test_create_from_yaml_v010.py` exits 0 with all 8 tests passing including the BEGIN/ROLLBACK transactional test
- `uv run test/integration/test_adbc_transactions.py` exits 0 (D-21 invariant after each Wave's tasks)
- `just test-all` exits 0
- `grep -c 'sv_register_table_function' cpp/src/shim.cpp` returns at least 2 (definition + at least one consumer)
- `grep -c '__sv_compute_create_from_yaml' cpp/src/shim.cpp` returns at least 1
- `grep -c '__sv_compute_create_from_yaml' src/parse.rs` returns at least 1
- `grep -nE 'catalog\.read_text|catalog\.lookup' src/parse.rs` returns 0 matches in the bodies of rewrite_yaml_file_create and rewrite_alter_comment (use line-range grep)
- No new package dependencies in Cargo.toml or PEP-723 test headers
</verification>

<success_criteria>
- DuckDB v1.5.2 `json_merge_patch` RFC-7396 null-as-delete confirmed by sqllogictest spike (Wave 0 gate)
- `sv_register_table_function` C++ Catalog API shim introduced from scratch (~150 LOC); A2 resolution honored
- `__sv_compute_create_from_yaml` helper TF registers, binds, opens per-call Connection(*context.db), invokes Rust FFI helper, returns enriched JSON in a single VARCHAR column
- ALTER SET COMMENT / UNSET COMMENT / RENAME TO ride pure-SQL rewrites on the caller's connection -- no catalog read
- CREATE FROM YAML FILE rides the helper-TF subquery -- no read_text on extension-owned connection
- D-21 transactional invariant: BEGIN/CREATE/ROLLBACK undoes YAML-based CREATE
- 8 enumerated future ALTER variants explicitly NOT implemented (A7 resolution -- Snowflake non-features)
- After Plan 04: OverrideContext CatalogReader has zero consumers under parser_override; H1 catalog_conn still allocated but truly unused (Plan 06 retires)
</success_criteria>

<output>
Create `.planning/phases/65-overridecontext-connection-teardown/65-04-SUMMARY.md` documenting:
- Wave 0 spike result: RFC-7396 null-as-delete confirmed (with actual outputs)
- C++ shim additions (LOC count, file size after vs before, RESEARCH §5.4 budget check)
- Rust FFI helper module + sv_compute_create_from_yaml_rust signature
- Decision: helper TF returns metadata-less JSON; outer INSERT wraps in json_merge_patch with now()/current_database()/current_schema() for metadata parity with non-YAML CREATE
- All sqllogictest counts (`just test-sql` summary)
- ADBC transactional test still green
- TECH-DEBT entries surfaced (e.g. if any v0.6.0 test required edits, or if any behavioral edge case was discovered)
- Forward pointer: "H1 catalog_conn allocation at src/lib.rs:386-410 still present but truly unused by every parser_override code path; Plan 06 retires the allocation."
</output>
