---
phase: 21-error-location-reporting
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/parse.rs
  - cpp/src/shim.cpp
autonomous: true
requirements: [ERR-01, ERR-02, ERR-03]

must_haves:
  truths:
    - "A malformed CREATE body with missing required clauses produces an error naming the missing clause"
    - "A malformed CREATE body with a misspelled clause keyword produces a 'Did you mean' suggestion"
    - "A near-miss DDL prefix like 'CREAT SEMANTIC VIEW' produces a 'Did you mean' suggestion"
    - "Error messages include a character position (byte offset) pointing at the problem location"
    - "The C++ sv_parse_stub returns DISPLAY_EXTENSION_ERROR with error_location for validation failures"
    - "Bracket/paren mismatch in CREATE body produces a positioned error"
    - "An empty CREATE body produces a specific error about required clauses"
  artifacts:
    - path: "src/parse.rs"
      provides: "ParseError struct, validate_ddl_body, detect_near_miss, validate_clauses, sv_validate_ddl_rust FFI"
      contains: "pub struct ParseError"
    - path: "cpp/src/shim.cpp"
      provides: "Tri-state sv_parse_stub calling sv_validate_ddl_rust with DISPLAY_EXTENSION_ERROR path"
      contains: "sv_validate_ddl_rust"
  key_links:
    - from: "cpp/src/shim.cpp"
      to: "src/parse.rs"
      via: "sv_validate_ddl_rust FFI call"
      pattern: "sv_validate_ddl_rust"
    - from: "cpp/src/shim.cpp"
      to: "DuckDB ParserExtensionParseResult"
      via: "DISPLAY_EXTENSION_ERROR with error_location"
      pattern: "error_location"
    - from: "src/parse.rs"
      to: "src/expand.rs"
      via: "suggest_closest reuse pattern (not direct import -- reimplements for &str slices)"
      pattern: "strsim::levenshtein"
---

<objective>
Implement the full error validation layer for semantic view DDL: ParseError struct with position tracking, clause-level validation for CREATE body, near-miss DDL prefix detection, and the FFI + C++ plumbing to route errors with character positions through DuckDB's DISPLAY_EXTENSION_ERROR path.

Purpose: Users currently get generic parse errors. After this plan, they get positioned, clause-specific errors with "did you mean" suggestions for typos.
Output: Modified src/parse.rs (validation logic + FFI) and cpp/src/shim.cpp (tri-state parse stub), verified by cargo test unit tests.
</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/21-error-location-reporting/21-CONTEXT.md
@.planning/phases/21-error-location-reporting/21-RESEARCH.md

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

From src/parse.rs (existing exports to preserve):
```rust
pub const PARSE_NOT_OURS: u8 = 0;
pub const PARSE_DETECTED: u8 = 1;

pub enum DdlKind {
    Create, CreateOrReplace, CreateIfNotExists,
    Drop, DropIfExists, Describe, Show,
}

pub fn detect_ddl_kind(query: &str) -> Option<DdlKind>;
pub fn detect_semantic_view_ddl(query: &str) -> u8;
pub fn rewrite_ddl(query: &str) -> Result<String, String>;
pub fn extract_ddl_name(query: &str) -> Result<Option<String>, String>;

// Existing FFI (keep for backward compat):
pub extern "C" fn sv_parse_rust(...) -> u8;
pub extern "C" fn sv_rewrite_ddl_rust(...) -> u8;
pub extern "C" fn sv_execute_ddl_rust(...) -> u8;

// Existing helpers used internally:
fn starts_with_ci(trimmed: &[u8], prefix: &[u8]) -> bool;
fn parse_create_body(trimmed: &str, prefix_len: usize) -> Result<(&str, &str), String>;
fn extract_name_only(trimmed: &str, prefix_len: usize) -> Result<String, String>;
fn function_name(kind: DdlKind) -> &'static str;
fn prefix_len(kind: DdlKind) -> usize;
```

From src/expand.rs (pattern to follow, not import):
```rust
pub fn suggest_closest(name: &str, available: &[String]) -> Option<String> {
    // Uses strsim::levenshtein, threshold <= 3
}
```

From src/query/error.rs (pattern to follow for "Did you mean" formatting):
```rust
// "Semantic view '{name}' not found. Did you mean '{s}'?"
```

From cpp/src/shim.cpp (current sv_parse_stub to modify):
```cpp
static ParserExtensionParseResult sv_parse_stub(
    ParserExtensionInfo *, const string &query) {
    uint8_t result = sv_parse_rust(
        reinterpret_cast<const char *>(query.c_str()), query.size());
    if (result == 1) {
        return ParserExtensionParseResult(
            make_uniq<SemanticViewParseData>(query));
    }
    return ParserExtensionParseResult();
}
```

From DuckDB amalgamation (verified in RESEARCH.md):
```cpp
// ParserExtensionParseResult has:
//   .type = DISPLAY_EXTENSION_ERROR
//   .error = string
//   .error_location = optional_idx (set via assignment)
// Error constructor: ParserExtensionParseResult(string error_p)
// DuckDB renders caret via ParserException::SyntaxError(query, result.error, result.error_location)
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: ParseError struct, validation functions, and near-miss detection</name>
  <files>src/parse.rs</files>
  <behavior>
    - validate_ddl_body with missing `tables` clause returns Err(ParseError) naming "tables" as required
    - validate_ddl_body with empty body "()" returns Err(ParseError) stating required clauses
    - validate_ddl_body with misspelled clause keyword (e.g., "tbles := [...]") returns "Did you mean 'tables'?"
    - validate_ddl_body with unbalanced brackets returns positioned error at the mismatch location
    - detect_near_miss("CREAT SEMANTIC VIEW x (...)") returns Some(ParseError) suggesting "CREATE SEMANTIC VIEW"
    - detect_near_miss("DROP SEMANTC VIEW x") returns Some(ParseError) suggesting "DROP SEMANTIC VIEW"
    - detect_near_miss("SHOW SEMANTIC VIEW") returns Some(ParseError) suggesting "SHOW SEMANTIC VIEWS"
    - detect_near_miss("SELECT 1") returns None (not a near-miss)
    - detect_near_miss("SHOW TABLES") returns None (edit distance too large)
    - ParseError.position is byte offset relative to the ORIGINAL query (before trimming)
    - Clause errors position points at the start of the clause keyword in the original query
    - Structural errors (missing name, missing parens) position points at end of prefix
  </behavior>
  <action>
Add a `ParseError` struct to `src/parse.rs`:
```rust
pub struct ParseError {
    pub message: String,
    pub position: Option<usize>,  // byte offset into original query
}
```

Add a `validate_ddl_body()` function that is called INSTEAD of `rewrite_ddl()` in the new FFI path. This function:
1. Calls `detect_ddl_kind()` -- if None, returns `Ok(None)` (not ours)
2. If DDL detected, validates structural correctness BEFORE rewriting:
   - For CREATE-with-body forms: calls `validate_clauses()` on the body
   - For name-only forms: validates name is present (existing logic, but return ParseError with position)
   - For SHOW: no validation needed (no arguments)
3. If validation passes, calls existing `rewrite_ddl()` and returns the SQL

The function signature:
```rust
pub fn validate_and_rewrite(query: &str) -> Result<Option<String>, ParseError>
// Ok(Some(sql)) = success, rewritten SQL
// Ok(None) = not our statement
// Err(ParseError) = validation error with message + position
```

Add `validate_clauses(body: &str, body_offset: usize, original_query: &str) -> Result<(), ParseError>`:
1. Check for empty body -- error: "Expected clause definitions (tables, dimensions, metrics). Body is empty."
2. Scan for `word :=` patterns at the top level (not inside single-quoted strings). For each word:
   - If it matches a known CLAUSE_KEYWORDS (`["tables", "relationships", "dimensions", "metrics"]`), it's valid
   - If not, use `strsim::levenshtein` (threshold <= 3) to suggest the closest keyword. Error: "Unknown clause '{word}'. Did you mean '{suggestion}'?"
   - Position: byte offset of the unknown word in the original query
3. Check required clauses: `tables` must be present. At least one of `dimensions` or `metrics` must be present. Error: "Missing required clause 'tables'" or "Missing required clause: at least one of 'dimensions' or 'metrics' must be specified"
4. Check for unbalanced parens/brackets: track `(`, `)`, `[`, `]` counts while respecting single-quoted strings. If mismatched, report position of the last unmatched opener.

Add `detect_near_miss(query: &str) -> Option<ParseError>`:
1. Extract the first N words of the query (up to 6 words to cover "CREATE SEMANTIC VIEW IF NOT EXISTS")
2. Compare against each of the 7 DDL_PREFIXES using `strsim::levenshtein`
3. If best match has edit distance <= 3, return `Some(ParseError)` with message: "Unknown statement. Did you mean '{prefix}'?" and position 0
4. If no match, return None

CRITICAL position tracking:
- Track `trim_offset = query.len() - query.trim_start().len()` at the start
- All positions calculated relative to the original query (add trim_offset when working with trimmed strings)
- Clause keyword positions: find the byte offset of the keyword in the original query string

Error message style per CONTEXT.md locked decisions:
- Identify the problem AND state what was expected
- No recovery hints
- No prefix -- messages stand alone
- Plain text only

Keep all existing public functions unchanged. The new validation layer is an ADDITION, not a replacement of existing logic. The `rewrite_ddl()` function continues to work as before -- `validate_and_rewrite()` wraps it with validation.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test -- parse 2>&1 | tail -20</automated>
  </verify>
  <done>ParseError struct exists. validate_and_rewrite() returns positioned errors for: empty body, missing required clauses, unknown clause keywords with suggestions, unbalanced brackets. detect_near_miss() catches typos in DDL prefixes. All positions are byte offsets relative to the original query. All existing parse tests still pass.</done>
</task>

<task type="auto">
  <name>Task 2: sv_validate_ddl_rust FFI and C++ tri-state sv_parse_stub</name>
  <files>src/parse.rs, cpp/src/shim.cpp</files>
  <action>
Add a new FFI entry point to `src/parse.rs`:

```rust
/// FFI entry point for DDL validation with error reporting.
///
/// Returns:
///   0 = success: rewritten SQL in sql_out
///   1 = error: error message in error_out, position in *position_out
///   2 = not ours: no output written
///
/// position_out is set to u32::MAX when no position is available.
#[cfg(feature = "extension")]
#[no_mangle]
pub extern "C" fn sv_validate_ddl_rust(
    query_ptr: *const u8,
    query_len: usize,
    sql_out: *mut u8,
    sql_out_len: usize,
    error_out: *mut u8,
    error_out_len: usize,
    position_out: *mut u32,
) -> u8 {
```

Implementation:
1. Wrap in `catch_unwind` for panic safety (same pattern as existing FFI functions)
2. Convert query_ptr to &str (same unsafe pattern as sv_parse_rust)
3. Call `validate_and_rewrite(query)`:
   - `Ok(Some(sql))` -> write sql to sql_out, return 0
   - `Ok(None)` -> check `detect_near_miss(query)`:
     - `Some(err)` -> write err.message to error_out, set position_out, return 1
     - `None` -> return 2 (not ours)
   - `Err(err)` -> write err.message to error_out, set position_out from err.position (or u32::MAX if None), return 1
4. On panic: return 2 (not ours -- let DuckDB show its normal error)

Modify `cpp/src/shim.cpp`:

1. Add the new FFI declaration:
```cpp
extern "C" {
    // ... existing declarations ...

    // DDL validation with error reporting: 0=success, 1=error, 2=not-ours
    uint8_t sv_validate_ddl_rust(
        const char *query_ptr, size_t query_len,
        char *sql_out, size_t sql_out_len,
        char *error_out, size_t error_out_len,
        uint32_t *position_out);
}
```

2. Replace `sv_parse_stub` implementation:
```cpp
static ParserExtensionParseResult sv_parse_stub(
    ParserExtensionInfo *, const string &query) {
    char sql_buf[4096];
    char error_buf[1024];
    uint32_t position = UINT32_MAX;
    memset(sql_buf, 0, sizeof(sql_buf));
    memset(error_buf, 0, sizeof(error_buf));

    uint8_t rc = sv_validate_ddl_rust(
        reinterpret_cast<const char *>(query.c_str()),
        query.size(),
        sql_buf, sizeof(sql_buf),
        error_buf, sizeof(error_buf),
        &position);

    if (rc == 0) {
        // Success: DDL detected and validated
        return ParserExtensionParseResult(
            make_uniq<SemanticViewParseData>(query));
    } else if (rc == 1) {
        // Error: validation failed, return extension error with position
        ParserExtensionParseResult err_result(string(error_buf));
        if (position != UINT32_MAX) {
            err_result.error_location = static_cast<idx_t>(position);
        }
        return err_result;
    }
    // rc == 2: not our statement
    return ParserExtensionParseResult();
}
```

Note: The sql_buf output from sv_validate_ddl_rust is NOT used in sv_parse_stub (the rewrite happens later in sv_ddl_bind via sv_rewrite_ddl_rust). The validation is the point -- we want to catch errors early in the parse phase where DISPLAY_EXTENSION_ERROR renders the caret. The existing sv_ddl_bind path (which calls sv_rewrite_ddl_rust) remains unchanged.

Keep `sv_parse_rust` FFI declaration and export -- it is still used by other code paths. The new `sv_validate_ddl_rust` is called ONLY from `sv_parse_stub`.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test -- parse 2>&1 | tail -20</automated>
  </verify>
  <done>sv_validate_ddl_rust FFI entry point compiles (feature-gated on "extension"). C++ sv_parse_stub calls sv_validate_ddl_rust and returns DISPLAY_EXTENSION_ERROR with error_location for validation failures. All existing cargo tests pass. The build succeeds with `just build`.</done>
</task>

</tasks>

<verification>
```bash
# Unit tests for all parse functions (existing + new validation)
cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test -- parse

# Build the extension to verify C++ compilation
cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && just build
```
</verification>

<success_criteria>
- ParseError struct with message + optional position exists in parse.rs
- validate_and_rewrite() validates CREATE body structure before rewriting
- detect_near_miss() catches DDL prefix typos with edit distance <= 3
- Clause keyword typos get "Did you mean" suggestions
- Missing required clauses produce specific error messages
- Unbalanced brackets produce positioned errors
- sv_validate_ddl_rust FFI returns tri-state (0/1/2) with error + position
- C++ sv_parse_stub returns DISPLAY_EXTENSION_ERROR with error_location on validation errors
- All positions are byte offsets relative to the original query string
- All existing tests pass (cargo test)
- Extension builds successfully (just build)
</success_criteria>

<output>
After completion, create `.planning/phases/21-error-location-reporting/21-01-SUMMARY.md`
</output>
