---
phase: 34.1-close-ddl-gaps-vs-snowflake
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/parse.rs
  - src/catalog.rs
  - src/ddl/mod.rs
  - src/ddl/alter.rs
  - src/lib.rs
  - test/sql/phase34_1_alter_rename.test
autonomous: true
requirements: [DDL-01, DDL-02]

must_haves:
  truths:
    - "ALTER SEMANTIC VIEW old_name RENAME TO new_name renames the view"
    - "ALTER SEMANTIC VIEW IF EXISTS missing_name RENAME TO x is a silent no-op"
    - "ALTER RENAME to an existing name returns an error"
    - "Renamed view persists across database restart"
    - "Renamed view is queryable under its new name"
  artifacts:
    - path: "src/ddl/alter.rs"
      provides: "AlterRenameVTab table function"
      exports: ["AlterRenameVTab", "AlterRenameState"]
    - path: "src/catalog.rs"
      provides: "catalog_rename function"
      exports: ["catalog_rename"]
    - path: "src/parse.rs"
      provides: "AlterRename and AlterRenameIfExists DdlKind variants"
      contains: "AlterRename"
  key_links:
    - from: "src/parse.rs"
      to: "src/ddl/alter.rs"
      via: "rewrite to SELECT * FROM alter_semantic_view_rename('old','new')"
      pattern: "alter_semantic_view_rename"
    - from: "src/ddl/alter.rs"
      to: "src/catalog.rs"
      via: "catalog_rename() call"
      pattern: "catalog_rename"
    - from: "src/lib.rs"
      to: "src/ddl/alter.rs"
      via: "register_table_function_with_extra_info"
      pattern: "AlterRenameVTab"
---

<objective>
Implement ALTER SEMANTIC VIEW ... RENAME TO and ALTER SEMANTIC VIEW IF EXISTS ... RENAME TO as new DDL verbs, following the established parser-hook + table-function pattern.

Purpose: Matches Snowflake's ALTER SEMANTIC VIEW scope (RENAME TO only). Allows users to rename semantic views without dropping and recreating them.
Output: Working ALTER RENAME DDL, catalog mutation (both in-memory and persistent), sqllogictest coverage.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/34.1-close-ddl-gaps-vs-snowflake-alter-semantic-view-show-semantic-dimensions-facts-metrics/34.1-RESEARCH.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From src/parse.rs:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DdlKind {
    Create,
    CreateOrReplace,
    CreateIfNotExists,
    Drop,
    DropIfExists,
    Describe,
    Show,
}

fn detect_ddl_prefix(trimmed: &str) -> Option<(DdlKind, usize)>;
fn match_keyword_prefix(input: &[u8], keywords: &[&[u8]]) -> Option<usize>;
fn function_name(kind: DdlKind) -> &'static str;
fn rewrite_ddl(query: &str) -> Result<String, String>;
pub fn validate_and_rewrite(query: &str) -> Result<Option<String>, ParseError>;
pub fn detect_near_miss(query: &str) -> Option<ParseError>;
const DDL_PREFIXES: &[&str];
```

From src/catalog.rs:
```rust
pub type CatalogState = Arc<RwLock<HashMap<String, String>>>;
pub fn catalog_insert(state: &CatalogState, name: &str, json: &str) -> Result<(), Box<dyn std::error::Error>>;
pub fn catalog_delete(state: &CatalogState, name: &str) -> Result<(), Box<dyn std::error::Error>>;
pub fn catalog_delete_if_exists(state: &CatalogState, name: &str);
pub fn catalog_upsert(state: &CatalogState, name: &str, json: &str) -> Result<(), Box<dyn std::error::Error>>;
```

From src/ddl/drop.rs (pattern for DropState + persist_drop):
```rust
#[derive(Clone)]
pub struct DropState {
    pub catalog: CatalogState,
    pub persist_conn: Option<ffi::duckdb_connection>,
    pub if_exists: bool,
}
fn persist_drop(conn: ffi::duckdb_connection, name: &str) { ... }
```

From src/lib.rs (registration pattern):
```rust
con.register_table_function_with_extra_info::<DropSemanticViewVTab, _>(
    "drop_semantic_view",
    &drop_state,
)?;
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add ALTER RENAME parser detection, catalog function, and VTab</name>
  <files>src/parse.rs, src/catalog.rs, src/ddl/mod.rs, src/ddl/alter.rs, src/lib.rs</files>
  <read_first>
    - src/parse.rs (current DdlKind enum, detect_ddl_prefix, DDL_PREFIXES, function_name, rewrite_ddl, validate_and_rewrite, detect_near_miss)
    - src/catalog.rs (catalog_delete, catalog_insert patterns, CatalogState type)
    - src/ddl/drop.rs (DropState, DropSemanticViewVTab, persist_drop -- pattern to replicate)
    - src/ddl/mod.rs (module declarations)
    - src/lib.rs (init_extension registration block near line 350-450)
  </read_first>
  <action>
**1. src/parse.rs -- Add DdlKind variants and prefix detection:**

Add two new variants to `DdlKind` enum:
```rust
pub enum DdlKind {
    // ... existing variants ...
    AlterRename,
    AlterRenameIfExists,
}
```

In `detect_ddl_prefix`, add BEFORE the existing `SHOW SEMANTIC VIEWS` check (longest-first):
```rust
// ALTER SEMANTIC VIEW IF EXISTS (5 keywords) -- before ALTER SEMANTIC VIEW
if let Some(n) = match_keyword_prefix(b, &[b"alter", b"semantic", b"view", b"if", b"exists"]) {
    return Some((DdlKind::AlterRenameIfExists, n));
}
// ALTER SEMANTIC VIEW (3 keywords)
if let Some(n) = match_keyword_prefix(b, &[b"alter", b"semantic", b"view"]) {
    return Some((DdlKind::AlterRename, n));
}
```

In `function_name`, add:
```rust
DdlKind::AlterRename => "alter_semantic_view_rename",
DdlKind::AlterRenameIfExists => "alter_semantic_view_rename_if_exists",
```

In `rewrite_ddl`, add a new match arm that parses `old_name RENAME TO new_name`:
```rust
DdlKind::AlterRename | DdlKind::AlterRenameIfExists => {
    let after_prefix = trimmed[plen..].trim();
    // Extract old_name (up to whitespace)
    let name_end = after_prefix.find(|c: char| c.is_whitespace()).ok_or("Missing view name after ALTER SEMANTIC VIEW")?;
    let old_name = &after_prefix[..name_end];
    let rest = after_prefix[name_end..].trim();
    // Expect RENAME TO
    let rest_upper = rest.to_ascii_uppercase();
    if !rest_upper.starts_with("RENAME TO") {
        return Err("Expected RENAME TO after view name".to_string());
    }
    let new_name = rest["RENAME TO".len()..].trim();
    if new_name.is_empty() {
        return Err("Missing new name after RENAME TO".to_string());
    }
    let safe_old = old_name.replace('\'', "''");
    let safe_new = new_name.replace('\'', "''");
    Ok(format!("SELECT * FROM {fn_name}('{safe_old}', '{safe_new}')"))
}
```

In `validate_and_rewrite`, add a new match arm for AlterRename/AlterRenameIfExists:
```rust
DdlKind::AlterRename | DdlKind::AlterRenameIfExists => {
    let after_prefix = trimmed_no_semi[plen..].trim();
    if after_prefix.is_empty() {
        return Err(ParseError {
            message: "Missing view name after ALTER SEMANTIC VIEW.".to_string(),
            position: Some(trim_offset + plen),
        });
    }
    // Validate RENAME TO is present
    let name_end = after_prefix.find(|c: char| c.is_whitespace())
        .ok_or_else(|| ParseError {
            message: "Expected RENAME TO after view name.".to_string(),
            position: Some(trim_offset + plen + after_prefix.len()),
        })?;
    let rest = after_prefix[name_end..].trim();
    let rest_upper = rest.to_ascii_uppercase();
    if !rest_upper.starts_with("RENAME TO") {
        return Err(ParseError {
            message: "Expected RENAME TO after view name. ALTER SEMANTIC VIEW only supports RENAME TO.".to_string(),
            position: Some(trim_offset + plen + name_end),
        });
    }
    let new_name_str = rest["RENAME TO".len()..].trim();
    if new_name_str.is_empty() {
        return Err(ParseError {
            message: "Missing new name after RENAME TO.".to_string(),
            position: Some(trim_offset + plen + after_prefix.len()),
        });
    }
    rewrite_ddl(query).map(Some).map_err(|e| ParseError {
        message: e,
        position: Some(trim_offset + plen),
    })
}
```

In `extract_ddl_name`, add:
```rust
DdlKind::AlterRename | DdlKind::AlterRenameIfExists => {
    let name = extract_name_only(trimmed, plen)?;
    Ok(Some(name))
}
```

Add to `DDL_PREFIXES`:
```rust
const DDL_PREFIXES: &[&str] = &[
    // ... existing ...
    "alter semantic view",
    "alter semantic view if exists",
];
```

Update the `/// The 7 supported DDL statement forms` doc comment to `/// The 9 supported DDL statement forms`.

**2. src/catalog.rs -- Add catalog_rename:**

Add a new public function:
```rust
/// Rename a semantic view in the in-memory catalog.
///
/// Atomically removes the old name and inserts the new name.
/// Returns an error if `old_name` does not exist or `new_name` already exists.
pub fn catalog_rename(
    state: &CatalogState,
    old_name: &str,
    new_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut guard = state.write().unwrap();
    let json = guard
        .remove(old_name)
        .ok_or_else(|| format!("semantic view '{}' does not exist", old_name))?;
    if guard.contains_key(new_name) {
        // Rollback: put the old entry back
        guard.insert(old_name.to_string(), json);
        return Err(format!("semantic view '{}' already exists", new_name).into());
    }
    guard.insert(new_name.to_string(), json);
    Ok(())
}
```

**3. src/ddl/alter.rs -- Create AlterRenameVTab:**

Create a new file modeled on `ddl/drop.rs`. Key elements:

```rust
use std::sync::atomic::{AtomicBool, Ordering};

use duckdb::{
    core::{DataChunkHandle, Inserter, LogicalTypeHandle, LogicalTypeId},
    vtab::{BindInfo, InitInfo, TableFunctionInfo, VTab},
};
use libduckdb_sys as ffi;
use std::ffi::CString;

use crate::catalog::{catalog_rename, CatalogState};

#[derive(Clone)]
pub struct AlterRenameState {
    pub catalog: CatalogState,
    pub persist_conn: Option<ffi::duckdb_connection>,
    pub if_exists: bool,
}

unsafe impl Send for AlterRenameState {}
unsafe impl Sync for AlterRenameState {}
```

The `persist_rename` function should:
1. DELETE the old row: `DELETE FROM semantic_layer._definitions WHERE name = '{old_name}'`
2. INSERT the new row: `INSERT INTO semantic_layer._definitions (name, definition) VALUES ('{new_name}', '{json}')`

The VTab bind function should:
1. Read two positional parameters: old_name (param 0) and new_name (param 1)
2. Read json from in-memory catalog for old_name
3. Check if old_name exists (if not, error unless if_exists)
4. Check if new_name already exists (error if so)
5. Persist first (DELETE old + INSERT new via persist_conn)
6. Then update in-memory catalog via `catalog_rename`
7. Output schema: two VARCHAR columns `old_name` and `new_name`

The `parameters()` function should return two VARCHAR parameters.

**4. src/ddl/mod.rs -- Add module declaration:**
```rust
pub mod alter;
```

**5. src/lib.rs -- Register the new table functions:**

After the existing `drop_semantic_view_if_exists` registration block, add:

```rust
// Register alter_semantic_view_rename(old, new) table function.
let alter_state = AlterRenameState {
    catalog: catalog_state.clone(),
    persist_conn,
    if_exists: false,
};
con.register_table_function_with_extra_info::<AlterRenameVTab, _>(
    "alter_semantic_view_rename",
    &alter_state,
)?;

// Register alter_semantic_view_rename_if_exists(old, new) table function.
let alter_if_exists_state = AlterRenameState {
    catalog: catalog_state.clone(),
    persist_conn,
    if_exists: true,
};
con.register_table_function_with_extra_info::<AlterRenameVTab, _>(
    "alter_semantic_view_rename_if_exists",
    &alter_if_exists_state,
)?;
```

Add the import at the top of the `init_extension` block:
```rust
use ddl::alter::{AlterRenameState, AlterRenameVTab};
```
  </action>
  <verify>
    <automated>cargo test 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - src/parse.rs contains `AlterRename,` and `AlterRenameIfExists,` in the DdlKind enum
    - src/parse.rs detect_ddl_prefix contains `match_keyword_prefix(b, &[b"alter", b"semantic", b"view", b"if", b"exists"])`
    - src/parse.rs DDL_PREFIXES contains `"alter semantic view"` and `"alter semantic view if exists"`
    - src/catalog.rs contains `pub fn catalog_rename(`
    - src/ddl/alter.rs exists and contains `pub struct AlterRenameVTab`
    - src/ddl/alter.rs contains `pub struct AlterRenameState`
    - src/ddl/alter.rs contains `fn persist_rename(`
    - src/ddl/mod.rs contains `pub mod alter;`
    - src/lib.rs contains `"alter_semantic_view_rename"`
    - src/lib.rs contains `"alter_semantic_view_rename_if_exists"`
    - `cargo test` exits 0
  </acceptance_criteria>
  <done>ALTER SEMANTIC VIEW ... RENAME TO parser detection, rewrite, catalog mutation, and VTab implementation compile and pass existing unit tests</done>
</task>

<task type="auto">
  <name>Task 2: Add sqllogictest coverage for ALTER RENAME</name>
  <files>test/sql/phase34_1_alter_rename.test</files>
  <read_first>
    - test/sql/phase20_extended_ddl.test (pattern for DDL testing -- DROP IF EXISTS, SHOW, error cases)
    - test/sql/phase2_restart.test (pattern for persistence across restart)
    - src/ddl/alter.rs (the implementation just created)
    - src/parse.rs (to verify the rewrite output format)
  </read_first>
  <action>
Create `test/sql/phase34_1_alter_rename.test` with the following test cases:

```
# Phase 34.1: ALTER SEMANTIC VIEW ... RENAME TO

require semantic_views

# --- Setup: create a view to rename ---
statement ok
CREATE TABLE p34_orders (id INTEGER PRIMARY KEY, amount DECIMAL(10,2), customer_id INTEGER);

statement ok
CREATE SEMANTIC VIEW sales_view AS
  TABLES (
    orders AS p34_orders PRIMARY KEY (id)
  )
  DIMENSIONS (
    order_id := orders.id
  )
  METRICS (
    total_amount := SUM(orders.amount)
  )

# --- Test 1: Basic RENAME ---
statement ok
ALTER SEMANTIC VIEW sales_view RENAME TO revenue_view

# Verify old name is gone
statement error
DESCRIBE SEMANTIC VIEW sales_view

# Verify new name works
query II
SHOW SEMANTIC VIEWS
----
revenue_view	p34_orders

# Verify query works under new name
query IR
SELECT * FROM semantic_view('revenue_view', dimensions := ['order_id'], metrics := ['total_amount'])
----

# --- Test 2: RENAME back ---
statement ok
ALTER SEMANTIC VIEW revenue_view RENAME TO sales_view

query II
SHOW SEMANTIC VIEWS
----
sales_view	p34_orders

# --- Test 3: RENAME non-existent (error) ---
statement error
ALTER SEMANTIC VIEW nonexistent_view RENAME TO something

# --- Test 4: IF EXISTS on non-existent (no-op) ---
statement ok
ALTER SEMANTIC VIEW IF EXISTS nonexistent_view RENAME TO something

# --- Test 5: RENAME to existing name (error) ---
statement ok
CREATE TABLE p34_products (id INTEGER PRIMARY KEY, name VARCHAR);

statement ok
CREATE SEMANTIC VIEW products_view AS
  TABLES (
    prods AS p34_products PRIMARY KEY (id)
  )
  DIMENSIONS (
    product_id := prods.id
  )
  METRICS (
    product_count := COUNT(prods.id)
  )

statement error
ALTER SEMANTIC VIEW sales_view RENAME TO products_view

# --- Test 6: Missing RENAME TO (error) ---
statement error
ALTER SEMANTIC VIEW sales_view

# --- Test 7: Missing new name (error) ---
statement error
ALTER SEMANTIC VIEW sales_view RENAME TO

# --- Test 8: Case insensitive DDL ---
statement ok
alter semantic view sales_view rename to my_sales

statement ok
ALTER SEMANTIC VIEW my_sales RENAME TO sales_view

# --- Cleanup ---
statement ok
DROP SEMANTIC VIEW sales_view

statement ok
DROP SEMANTIC VIEW products_view

statement ok
DROP TABLE p34_orders

statement ok
DROP TABLE p34_products
```
  </action>
  <verify>
    <automated>just build && just test-sql 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - test/sql/phase34_1_alter_rename.test exists
    - test/sql/phase34_1_alter_rename.test contains `ALTER SEMANTIC VIEW sales_view RENAME TO revenue_view`
    - test/sql/phase34_1_alter_rename.test contains `ALTER SEMANTIC VIEW IF EXISTS nonexistent_view RENAME TO`
    - test/sql/phase34_1_alter_rename.test contains `statement error` for rename-to-existing and rename-nonexistent cases
    - `just build && just test-sql` exits 0
  </acceptance_criteria>
  <done>ALTER RENAME has sqllogictest coverage for: basic rename, rename back, non-existent error, IF EXISTS no-op, conflict error, missing RENAME TO error, missing new name error, case insensitivity</done>
</task>

</tasks>

<verification>
- `cargo test` passes (all existing + new unit tests)
- `just build && just test-sql` passes (new sqllogictest file)
- ALTER SEMANTIC VIEW x RENAME TO y changes the view name in catalog
- ALTER SEMANTIC VIEW IF EXISTS missing RENAME TO y is a silent no-op
- ALTER SEMANTIC VIEW x RENAME TO existing_name produces an error
- Near-miss detection suggests "ALTER SEMANTIC VIEW" for typos
</verification>

<success_criteria>
- AlterRename and AlterRenameIfExists DdlKind variants exist in parse.rs
- alter.rs VTab module implements the full ALTER RENAME lifecycle
- catalog_rename function handles atomic in-memory rename
- persist_rename handles persistent catalog rename via persist_conn
- sqllogictest covers all 8 test scenarios
- `just test-all` passes
</success_criteria>

<output>
After completion, create `.planning/phases/34.1-close-ddl-gaps-vs-snowflake-alter-semantic-view-show-semantic-dimensions-facts-metrics/34.1-01-SUMMARY.md`
</output>
