---
phase: 16-parser-hook-registration
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/parse.rs
  - src/lib.rs
  - cpp/src/shim.cpp
autonomous: true
requirements: [PARSE-01, PARSE-02, PARSE-03, PARSE-04, PARSE-05]

must_haves:
  truths:
    - "Rust detect_create_semantic_view() correctly identifies CREATE SEMANTIC VIEW with case variations, leading whitespace, and trailing semicolons"
    - "Rust detect_create_semantic_view() returns PARSE_NOT_OURS for all non-matching statements including SELECT, CREATE TABLE, CREATE VIEW, empty strings"
    - "sv_parse_rust FFI entry point wraps detection in catch_unwind so panics cannot cross the FFI boundary"
    - "C++ sv_parse_stub delegates to Rust sv_parse_rust instead of doing its own detection"
    - "Normal SQL statements pass through with zero overhead (DISPLAY_ORIGINAL_ERROR returned immediately)"
    - "CREATE SEMANTIC VIEW triggers the existing stub plan function and returns 'CREATE SEMANTIC VIEW stub fired'"
  artifacts:
    - path: "src/parse.rs"
      provides: "Pure Rust detection function + feature-gated FFI entry point"
      exports: ["detect_create_semantic_view", "sv_parse_rust", "PARSE_NOT_OURS", "PARSE_DETECTED"]
      min_lines: 50
    - path: "src/lib.rs"
      provides: "Module declaration for parse"
      contains: "pub mod parse"
    - path: "cpp/src/shim.cpp"
      provides: "C++ trampoline calling Rust via extern C FFI"
      contains: "sv_parse_rust"
  key_links:
    - from: "cpp/src/shim.cpp"
      to: "src/parse.rs"
      via: "extern C FFI call to sv_parse_rust"
      pattern: "sv_parse_rust"
    - from: "src/parse.rs"
      to: "src/parse.rs"
      via: "sv_parse_rust calls detect_create_semantic_view"
      pattern: "detect_create_semantic_view"
---

<objective>
Replace the C++ `CREATE SEMANTIC VIEW` detection logic in `sv_parse_stub` with a Rust implementation called via FFI trampoline. The C++ side becomes a thin caller; all detection logic lives in a pure, testable Rust function with `catch_unwind` panic safety at the FFI boundary.

Purpose: Move detection logic to Rust for testability (unit tests under `cargo test`), panic safety (`catch_unwind`), and preparation for Phase 17 where Rust will parse the DDL body.

Output: `src/parse.rs` (Rust detection + FFI), modified `cpp/src/shim.cpp` (trampoline), modified `src/lib.rs` (module declaration).
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/16-parser-hook-registration/16-CONTEXT.md
@.planning/phases/16-parser-hook-registration/16-RESEARCH.md
@.planning/phases/15-entry-point-poc/15-02-SUMMARY.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
<!-- Executor should use these directly -- no codebase exploration needed. -->

From cpp/src/shim.cpp (current sv_parse_stub to be modified):
```c++
// SemanticViewParseData carries query text from parse to plan.
struct SemanticViewParseData : public ParserExtensionParseData {
    string query;
    explicit SemanticViewParseData(string q) : query(std::move(q)) {}
    unique_ptr<ParserExtensionParseData> Copy() const override {
        return make_uniq<SemanticViewParseData>(query);
    }
    string ToString() const override { return query; }
};

// Current C++ detection -- TO BE REPLACED with Rust FFI call:
static ParserExtensionParseResult sv_parse_stub(
    ParserExtensionInfo *, const string &query) {
    string trimmed = query;
    StringUtil::Trim(trimmed);
    trimmed = StringUtil::Lower(trimmed);
    if (StringUtil::StartsWith(trimmed, "create semantic view")) {
        return ParserExtensionParseResult(
            make_uniq<SemanticViewParseData>(query));
    }
    return ParserExtensionParseResult();
}
```

From src/lib.rs (module structure):
```rust
pub mod catalog;
pub mod expand;
pub mod model;
#[cfg(feature = "extension")]
pub mod query;
```

From build.rs (C++ compilation -- no changes needed):
```rust
cc::Build::new()
    .cpp(true)
    .std("c++17")
    .include("cpp/include")
    .file("cpp/include/duckdb.cpp")
    .file("cpp/src/shim.cpp")
    .warnings(false)
    .compile("semantic_views_shim");
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create Rust parse module with detection function and FFI entry point</name>
  <files>src/parse.rs, src/lib.rs</files>
  <behavior>
    - detect_create_semantic_view("CREATE SEMANTIC VIEW test (...)") returns PARSE_DETECTED (1)
    - detect_create_semantic_view("create semantic view test") returns PARSE_DETECTED (1) (case insensitive)
    - detect_create_semantic_view("Create Semantic View test") returns PARSE_DETECTED (1) (mixed case)
    - detect_create_semantic_view("  CREATE SEMANTIC VIEW test") returns PARSE_DETECTED (1) (leading whitespace)
    - detect_create_semantic_view("\n\tCREATE SEMANTIC VIEW test") returns PARSE_DETECTED (1) (leading newlines/tabs)
    - detect_create_semantic_view("CREATE SEMANTIC VIEW test;") returns PARSE_DETECTED (1) (trailing semicolon)
    - detect_create_semantic_view("CREATE SEMANTIC VIEW test ;") returns PARSE_DETECTED (1) (semicolon with space)
    - detect_create_semantic_view("SELECT 1") returns PARSE_NOT_OURS (0)
    - detect_create_semantic_view("CREATE TABLE test") returns PARSE_NOT_OURS (0)
    - detect_create_semantic_view("CREATE VIEW test") returns PARSE_NOT_OURS (0)
    - detect_create_semantic_view("") returns PARSE_NOT_OURS (0)
    - detect_create_semantic_view(";") returns PARSE_NOT_OURS (0)
    - detect_create_semantic_view("CREATE") returns PARSE_NOT_OURS (0)
    - detect_create_semantic_view("create semantic vie") returns PARSE_NOT_OURS (0) (too short by 1 char)
  </behavior>
  <action>
1. Create `src/parse.rs` with the following structure (reference RESEARCH.md "Complete Rust Parse Module" code example):

   a. Define constants: `pub const PARSE_NOT_OURS: u8 = 0;` and `pub const PARSE_DETECTED: u8 = 1;`

   b. Implement `pub fn detect_create_semantic_view(query: &str) -> u8`:
      - `query.trim()` to strip leading/trailing whitespace
      - `.trim_end_matches(';').trim()` to strip trailing semicolons (DuckDB issue #18485 inconsistency)
      - Length check: if trimmed < 20 chars ("create semantic view" is 20), return PARSE_NOT_OURS
      - Compare first 20 bytes case-insensitively via `[u8]::eq_ignore_ascii_case`
      - Return PARSE_DETECTED or PARSE_NOT_OURS

   c. Implement the FFI entry point, feature-gated on `extension`:
      ```rust
      #[cfg(feature = "extension")]
      #[no_mangle]
      pub extern "C" fn sv_parse_rust(query_ptr: *const u8, query_len: usize) -> u8
      ```
      - Wrap body in `std::panic::catch_unwind(AssertUnwindSafe(|| { ... }))`
      - Null/zero-length check returns PARSE_NOT_OURS
      - Convert pointer+length to `&str` via `from_utf8_unchecked` + `from_raw_parts` (SAFETY: DuckDB query text is always valid UTF-8 ASCII SQL)
      - Call `detect_create_semantic_view(query)`
      - `.unwrap_or(PARSE_NOT_OURS)` on catch_unwind result (on panic, treat as not ours)

   d. Add `#[cfg(test)] mod tests` with unit tests covering all behaviors listed above.

2. Add `pub mod parse;` to `src/lib.rs` -- NOT feature-gated (the pure detection function must be testable under `cargo test` without the extension feature). Place it after `pub mod model;` and before the `#[cfg(feature = "extension")] pub mod query;` line.

Note: The `sv_parse_rust` FFI function IS feature-gated inside parse.rs (`#[cfg(feature = "extension")]`), so `cargo test` compiles the module but skips the FFI entry point. This avoids linker errors for missing C++ symbols during `cargo test`.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test parse -- --nocapture</automated>
  </verify>
  <done>All 7+ unit tests pass under cargo test. detect_create_semantic_view correctly handles case variations, whitespace, semicolons, and non-matching queries. FFI entry point compiles under extension feature gate.</done>
</task>

<task type="auto">
  <name>Task 2: Modify C++ trampoline to call Rust FFI and add sqllogictest</name>
  <files>cpp/src/shim.cpp, test/sql/phase16_parser.test, test/sql/TEST_LIST</files>
  <action>
1. Modify `cpp/src/shim.cpp` -- replace the detection logic in `sv_parse_stub` with a call to Rust:

   a. Add Rust FFI declaration before sv_parse_stub:
      ```c++
      // Rust FFI -- parse detection (defined in src/parse.rs)
      extern "C" {
          uint8_t sv_parse_rust(const char *query, size_t query_len);
      }
      ```

   b. Replace the body of `sv_parse_stub` with:
      ```c++
      static ParserExtensionParseResult sv_parse_stub(
          ParserExtensionInfo *, const string &query) {
          // Delegate detection to Rust
          uint8_t result = sv_parse_rust(
              reinterpret_cast<const char *>(query.c_str()),
              query.size());
          if (result == 1) {
              // Rust detected CREATE SEMANTIC VIEW -- carry query text forward
              return ParserExtensionParseResult(
                  make_uniq<SemanticViewParseData>(query));
          }
          // Not our statement -- let DuckDB show its normal error
          return ParserExtensionParseResult();
      }
      ```

   c. Remove the old detection code (StringUtil::Trim, StringUtil::Lower, StringUtil::StartsWith).

   d. Keep ALL other code unchanged: SemanticViewParseData, sv_plan_stub (bind/execute/plan), sv_register_parser_hooks.

2. Create `test/sql/phase16_parser.test` sqllogictest:
   ```sql
   # Phase 16: Parser hook FFI trampoline integration test
   # Verifies that CREATE SEMANTIC VIEW detection is handled by Rust via FFI
   # and normal SQL passes through unaffected.

   require semantic_views

   # Normal SQL still works (PARSE-03: zero overhead for non-extension queries)
   query I
   SELECT 42;
   ----
   42

   # Parser hook fires for CREATE SEMANTIC VIEW (PARSE-01, PARSE-02, PARSE-04)
   # The stub plan function returns "CREATE SEMANTIC VIEW stub fired"
   query T
   CREATE SEMANTIC VIEW test_view (tables := [], dimensions := [], metrics := []);
   ----
   CREATE SEMANTIC VIEW stub fired

   # Case insensitive detection (PARSE-02)
   query T
   create semantic view lower_test (tables := [], dimensions := [], metrics := []);
   ----
   CREATE SEMANTIC VIEW stub fired

   # Mixed case detection (PARSE-02)
   query T
   Create Semantic View mixed_test (tables := [], dimensions := [], metrics := []);
   ----
   CREATE SEMANTIC VIEW stub fired

   # Normal CREATE TABLE still works (PARSE-03)
   statement ok
   CREATE TABLE parser_test_16 (id INTEGER);

   statement ok
   DROP TABLE parser_test_16;
   ```

3. Add `test/sql/phase16_parser.test` to `test/sql/TEST_LIST` (append a new line).

4. Build and run full test suite:
   - `just build` (rebuilds extension with modified shim.cpp + new parse.rs)
   - `just test-all` (Rust unit tests + sqllogictest + DuckLake CI)
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && just build && just test-all</automated>
  </verify>
  <done>C++ trampoline calls Rust sv_parse_rust instead of doing its own detection. sqllogictest proves the full hook chain works end-to-end: CREATE SEMANTIC VIEW triggers stub, normal SQL passes through. `just test-all` passes with no regressions.</done>
</task>

</tasks>

<verification>
Phase-level verification (run after all tasks complete):

1. `cargo test parse` -- Rust unit tests for detection logic pass
2. `just build && just test-all` -- Full suite passes (Rust + sqllogictest + DuckLake CI)
3. Confirm no regressions: existing sqllogictest suite (phase2_ddl, phase4_query, semantic_views) still passes
4. Confirm PARSE-01 through PARSE-05 requirements are covered:
   - PARSE-01: parse_function still registered as fallback (sv_register_parser_hooks unchanged)
   - PARSE-02: case-insensitive detection with whitespace/semicolons (Rust unit tests + sqllogictest)
   - PARSE-03: DISPLAY_ORIGINAL_ERROR for normal SQL (sqllogictest: SELECT 42 works, CREATE TABLE works)
   - PARSE-04: C++ trampoline calls Rust extern "C" (integration proven by sqllogictest)
   - PARSE-05: catch_unwind in sv_parse_rust (code review + unit test coverage of edge cases)
</verification>

<success_criteria>
- `cargo test parse` passes with 7+ unit tests covering all case variations, whitespace, semicolons, and non-matching input
- `just test-all` passes (0 regressions from Phase 15)
- phase16_parser.test sqllogictest proves CREATE SEMANTIC VIEW triggers Rust-side detection via FFI trampoline
- Normal SQL (SELECT, CREATE TABLE) passes through with DISPLAY_ORIGINAL_ERROR (zero overhead)
- src/parse.rs exists with pure detection function (not feature-gated) and FFI entry (feature-gated)
- cpp/src/shim.cpp sv_parse_stub calls sv_parse_rust instead of using StringUtil detection
</success_criteria>

<output>
After completion, create `.planning/phases/16-parser-hook-registration/16-01-SUMMARY.md`
</output>
