---
phase: 34.1-close-ddl-gaps-vs-snowflake
plan: 03
type: execute
wave: 2
depends_on: ["34.1-02"]
files_modified:
  - src/parse.rs
  - src/ddl/mod.rs
  - src/ddl/show_dims_for_metric.rs
  - src/lib.rs
  - test/sql/phase34_1_show_dims_for_metric.test
autonomous: true
requirements: [DDL-07]

must_haves:
  truths:
    - "SHOW SEMANTIC DIMENSIONS FOR METRIC metric_name IN view_name returns only fan-trap-safe dimensions"
    - "Dimensions on the base table are always returned (no source_table)"
    - "Dimensions reachable via ManyToOne joins from the metric's table are returned"
    - "Dimensions that would cause a fan trap are excluded"
    - "Derived metrics resolve their source tables transitively"
  artifacts:
    - path: "src/ddl/show_dims_for_metric.rs"
      provides: "ShowDimensionsForMetricVTab"
      exports: ["ShowDimensionsForMetricVTab"]
  key_links:
    - from: "src/parse.rs"
      to: "src/ddl/show_dims_for_metric.rs"
      via: "rewrite to SELECT * FROM show_semantic_dimensions_for_metric('view', 'metric')"
      pattern: "show_semantic_dimensions_for_metric"
    - from: "src/ddl/show_dims_for_metric.rs"
      to: "src/expand.rs"
      via: "reuses ancestors_to_root, collect_derived_metric_source_tables, check_fan_traps logic"
      pattern: "fan_trap|ancestors_to_root"
    - from: "src/ddl/show_dims_for_metric.rs"
      to: "src/graph.rs"
      via: "RelationshipGraph::from_definition"
      pattern: "RelationshipGraph"
---

<objective>
Implement SHOW SEMANTIC DIMENSIONS FOR METRIC metric_name IN view_name, which returns dimensions that are fan-trap-safe for a given metric. This is the most complex SHOW command because it requires graph traversal and fan trap detection to determine which dimensions can legally accompany a given metric.

Purpose: Per Snowflake's semantics, "the logical table for the dimension must have an equal or lower level of granularity than the logical table for the metric." This command previews what `semantic_view()` would accept for a given metric.
Output: Working SHOW DIMENSIONS FOR METRIC command with fan-trap-aware filtering, 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
@.planning/phases/34.1-close-ddl-gaps-vs-snowflake-alter-semantic-view-show-semantic-dimensions-facts-metrics/34.1-02-SUMMARY.md

<interfaces>
<!-- Key types and contracts from Plan 02 output and existing codebase. -->

From src/ddl/show_dims.rs (created by Plan 02):
```rust
pub struct ShowDimRow {
    semantic_view_name: String,
    name: String,
    expr: String,
    source_table: String,
    data_type: String,
}
// Pattern: AtomicBool done, bind reads from CatalogState, parameters -> Vec<LogicalTypeHandle>
```

From src/expand.rs (functions to reuse -- these are currently private):
```rust
fn collect_derived_metric_source_tables(
    met: &crate::model::Metric,
    all_metrics: &[crate::model::Metric],
) -> Vec<String>;

fn ancestors_to_root(node: &str, parent_map: &HashMap<String, String>) -> Vec<String>;

fn check_fan_traps(
    view_name: &str,
    def: &SemanticViewDefinition,
    resolved_dims: &[&crate::model::Dimension],
    resolved_mets: &[&crate::model::Metric],
) -> Result<(), ExpandError>;
```

From src/graph.rs:
```rust
pub struct RelationshipGraph {
    pub edges: HashMap<String, Vec<String>>,
    pub reverse: HashMap<String, Vec<String>>,
    pub all_nodes: HashSet<String>,
    pub root: String,
}
impl RelationshipGraph {
    pub fn from_definition(def: &SemanticViewDefinition) -> Result<Self, String>;
}
```

From src/model.rs:
```rust
pub struct Dimension { pub name: String, pub expr: String, pub source_table: Option<String>, pub output_type: Option<String> }
pub struct Metric { pub name: String, pub expr: String, pub source_table: Option<String>, pub output_type: Option<String>, pub using_relationships: Vec<String> }
pub enum Cardinality { ManyToOne, OneToOne }
pub struct Join { pub table: String, pub from_alias: String, pub fk_columns: Vec<String>, pub cardinality: Cardinality, ... }
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add SHOW DIMS FOR METRIC parser detection, fan-trap-aware VTab, and registration</name>
  <files>src/parse.rs, src/expand.rs, src/ddl/mod.rs, src/ddl/show_dims_for_metric.rs, src/lib.rs</files>
  <read_first>
    - src/parse.rs (current state after Plans 01+02 -- DdlKind enum, detect_ddl_prefix, rewrite_ddl, validate_and_rewrite)
    - src/expand.rs (ancestors_to_root at line 1112, collect_derived_metric_source_tables at line 936, check_fan_traps at line 1010, check_path_up, check_path_down, path_from_ancestor_to_node -- understand the full fan trap algorithm)
    - src/graph.rs (RelationshipGraph::from_definition)
    - src/model.rs (SemanticViewDefinition, Dimension, Metric, Join, Cardinality)
    - src/ddl/show_dims.rs (ShowDimRow struct and VTab pattern from Plan 02)
    - src/ddl/mod.rs (current module declarations)
    - src/lib.rs (registration block)
  </read_first>
  <action>
**1. src/expand.rs -- Make helper functions pub(crate):**

The fan trap detection logic lives in `expand.rs` as private functions. Rather than duplicating it, change the visibility of these functions from `fn` to `pub(crate) fn`:

- `collect_derived_metric_source_tables` (line ~936): change `fn` to `pub(crate) fn`
- `ancestors_to_root` (line ~1112): change `fn` to `pub(crate) fn`

These are the two functions needed by the SHOW DIMS FOR METRIC VTab. The full `check_fan_traps` function is NOT reused directly because SHOW needs to filter dimensions (return a list), not validate a request (return error). Instead, the VTab implements its own filtering loop using the same building blocks.

**2. src/parse.rs -- Add ShowDimensionsForMetric variant:**

Add to `DdlKind`:
```rust
ShowDimensionsForMetric,
```

In `detect_ddl_prefix`, the detection is tricky because the full syntax is `SHOW SEMANTIC DIMENSIONS ... FOR METRIC ...`. The approach: use the existing `ShowDimensions` detection (3 keywords), then in `rewrite_ddl` check if the remainder contains `FOR METRIC`. This avoids needing a 5-keyword prefix match that interleaves with `IN`.

So no change to `detect_ddl_prefix` is needed. Instead, modify the `rewrite_ddl` handler for `ShowDimensions` to detect the `FOR METRIC` pattern:

In the `DdlKind::ShowDimensions` arm of `rewrite_ddl`, after parsing the `IN view_name` portion, check if the remainder contains `FOR METRIC`:

```rust
DdlKind::ShowDimensions => {
    let after_prefix = trimmed[plen..].trim();
    if after_prefix.is_empty() {
        // Cross-view form: SHOW SEMANTIC DIMENSIONS
        return Ok(format!("SELECT * FROM show_semantic_dimensions_all()"));
    }
    let upper = after_prefix.to_ascii_uppercase();

    // Check for "FOR METRIC" (without IN) -- error: view name required
    if upper.starts_with("FOR") {
        return Err("SHOW SEMANTIC DIMENSIONS FOR METRIC requires IN view_name. Usage: SHOW SEMANTIC DIMENSIONS IN view_name FOR METRIC metric_name".to_string());
    }

    // Must start with IN
    if !upper.starts_with("IN") || (after_prefix.len() > 2 && !after_prefix.as_bytes()[2].is_ascii_whitespace()) {
        return Err("Expected IN after SHOW SEMANTIC DIMENSIONS. Usage: SHOW SEMANTIC DIMENSIONS [IN view_name]".to_string());
    }
    let after_in = after_prefix[2..].trim();
    if after_in.is_empty() {
        return Err("Missing view name after IN".to_string());
    }
    // Extract view name (up to whitespace or end)
    let name_end = after_in.find(|c: char| c.is_whitespace()).unwrap_or(after_in.len());
    let view_name = &after_in[..name_end];
    let rest_after_name = after_in[name_end..].trim();

    if rest_after_name.is_empty() {
        // Simple form: SHOW SEMANTIC DIMENSIONS IN view_name
        let safe_name = view_name.replace('\'', "''");
        return Ok(format!("SELECT * FROM show_semantic_dimensions('{safe_name}')"));
    }

    // Check for FOR METRIC
    let rest_upper = rest_after_name.to_ascii_uppercase();
    if rest_upper.starts_with("FOR METRIC") || rest_upper.starts_with("FOR  METRIC") {
        // Find start of metric name after "FOR METRIC"
        let for_metric_prefix = if rest_upper.starts_with("FOR METRIC") { "FOR METRIC" } else { "FOR  METRIC" };
        let after_for_metric = rest_after_name[for_metric_prefix.len()..].trim();
        // More robust: find "FOR" then skip whitespace, find "METRIC" then skip whitespace
        // Let's do it properly:
        let rest_bytes = rest_after_name.as_bytes();
        let mut pos = 0;
        // Skip "FOR"
        if rest_bytes.len() < 3 || !rest_bytes[..3].eq_ignore_ascii_case(b"for") {
            return Err("Expected FOR METRIC after view name".to_string());
        }
        pos = 3;
        // Skip whitespace
        while pos < rest_bytes.len() && rest_bytes[pos].is_ascii_whitespace() { pos += 1; }
        // Skip "METRIC"
        if rest_bytes.len() < pos + 6 || !rest_bytes[pos..pos+6].eq_ignore_ascii_case(b"metric") {
            return Err("Expected FOR METRIC after view name".to_string());
        }
        pos += 6;
        // Skip whitespace
        while pos < rest_bytes.len() && rest_bytes[pos].is_ascii_whitespace() { pos += 1; }
        let metric_name = rest_after_name[pos..].trim();
        if metric_name.is_empty() {
            return Err("Missing metric name after FOR METRIC".to_string());
        }
        let safe_view = view_name.replace('\'', "''");
        let safe_metric = metric_name.replace('\'', "''");
        return Ok(format!("SELECT * FROM show_semantic_dimensions_for_metric('{safe_view}', '{safe_metric}')"));
    }

    Err(format!("Unexpected tokens after view name: '{}'. Usage: SHOW SEMANTIC DIMENSIONS IN view_name [FOR METRIC metric_name]", rest_after_name))
}
```

Note: Since the `FOR METRIC` form is detected within the `ShowDimensions` rewrite handler, we do NOT need a separate `ShowDimensionsForMetric` DdlKind variant. Remove it from the enum if already added. The detection happens at rewrite time, not prefix time.

**3. src/ddl/show_dims_for_metric.rs -- Create fan-trap-aware VTab:**

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

use duckdb::{
    core::{DataChunkHandle, Inserter, LogicalTypeHandle, LogicalTypeId},
    vtab::{BindInfo, InitInfo, TableFunctionInfo, VTab},
};

use crate::catalog::CatalogState;
use crate::expand::{ancestors_to_root, collect_derived_metric_source_tables};
use crate::graph::RelationshipGraph;
use crate::model::{Cardinality, SemanticViewDefinition};
```

Output schema (5 VARCHAR columns, same as SHOW DIMS):
- `semantic_view_name`
- `name`
- `expr`
- `source_table`
- `data_type`

Takes two VARCHAR parameters: view_name, metric_name.

**Core algorithm in bind (fan-trap-aware filtering):**

1. Deserialize the definition via `SemanticViewDefinition::from_json(view_name, json_str)`
2. Find the metric by name (case-insensitive). Error if not found with suggestion via `strsim`.
3. Get metric's source tables: if `source_table` is Some, use it; if None (derived), call `collect_derived_metric_source_tables`
4. If no joins exist, all dimensions are reachable (single-table view)
5. Otherwise, build `RelationshipGraph::from_definition`
6. Build `parent_map` from `graph.reverse` (each non-root node's parent)
7. Build `card_map`: `(from_lower, to_lower) -> Cardinality` from joins
8. For each dimension:
   - If `source_table` is None (base table dimension), it is always reachable
   - Otherwise, for each metric source table, check if a fan-out would occur on the path between metric table and dimension table:
     - Find ancestors of both metric table and dimension table
     - Find LCA (lowest common ancestor)
     - Walk path from metric table UP to LCA: check each edge. If edge is ManyToOne and we traverse it in the reverse direction (from target to source), that's a fan-out. Specifically: if the edge in card_map is `(current, parent)` with ManyToOne, walking from parent->current fans out. If the edge is `(parent, current)` with ManyToOne, walking current->parent is safe.
     - Walk path from LCA DOWN to dimension table: same fan-out checks
   - A dimension is reachable if NO fan-out is detected on ANY path between ANY metric source table and the dimension's table

The algorithm mirrors `check_fan_traps` in expand.rs. The key difference: instead of returning an error on the first fan-out, we skip that dimension and continue to the next.

**Implementation approach:**
```rust
fn is_dimension_reachable_for_metric(
    dim: &crate::model::Dimension,
    met_tables: &[String],
    graph: &RelationshipGraph,
    parent_map: &HashMap<String, String>,
    card_map: &HashMap<(String, String), Cardinality>,
) -> bool {
    // Base table dimension: always reachable
    let Some(ref dim_table_raw) = dim.source_table else {
        return true;
    };
    let dim_table = dim_table_raw.to_ascii_lowercase();

    for met_table in met_tables {
        if *met_table == dim_table {
            return true; // Same table, no fan-out possible
        }

        let met_ancestors = ancestors_to_root(met_table, parent_map);
        let dim_ancestors = ancestors_to_root(&dim_table, parent_map);

        let dim_ancestor_set: HashSet<&String> = dim_ancestors.iter().collect();
        let lca = met_ancestors.iter().find(|a| dim_ancestor_set.contains(a));

        let Some(lca) = lca else {
            continue; // No common ancestor (disconnected -- shouldn't happen in valid tree)
        };

        // Check path UP from met_table to LCA
        let mut current = met_table.clone();
        let mut fan_out = false;
        while current != *lca {
            let Some(parent) = parent_map.get(&current) else { break; };
            // Edge direction: check (current, parent) or (parent, current) in card_map
            if let Some(&card) = card_map.get(&(parent.clone(), current.clone())) {
                // Edge is parent->current. Walking current->parent (upward).
                // If parent->current is ManyToOne, walking backward (current->parent) means
                // we go from the "many" side to the "one" side -- safe.
                // ManyToOne means current is "many" side, parent is "one" -- safe direction
                // (no fan-out going up from many to one)
            }
            if let Some(&card) = card_map.get(&(current.clone(), parent.clone())) {
                // Edge is current->parent. Walking current->parent (upward).
                // If current->parent is ManyToOne, that's safe (going to the "one" side).
            }
            // Fan-out occurs when walking FROM the "one" side TO the "many" side.
            // The edge `(from, to)` with ManyToOne means `from` is many, `to` is one.
            // Walking from `to` to `from` (one->many) = fan-out.
            // In our upward walk: current->parent.
            // If card_map has (parent, current) with ManyToOne:
            //   parent is many, current is one. Walking current->parent = one->many = FAN OUT
            if let Some(&card) = card_map.get(&(parent.clone(), current.clone())) {
                if card == Cardinality::ManyToOne {
                    // parent->current edge: parent is "many" side, current is "one" side (reference target)
                    // Walking from current (one) to parent (many) = fan-out
                    fan_out = true;
                    break;
                }
            }
            current = parent.clone();
        }

        if fan_out {
            return false;
        }

        // Check path DOWN from LCA to dim_table
        // Build path from LCA to dim_table using dim_ancestors
        let dim_ancestors_vec = ancestors_to_root(&dim_table, parent_map);
        if let Some(lca_pos) = dim_ancestors_vec.iter().position(|a| a == lca) {
            let path_down: Vec<String> = dim_ancestors_vec[..=lca_pos].iter().rev().cloned().collect();
            for window in path_down.windows(2) {
                let from = &window[0];
                let to = &window[1];
                // Walking from->to (downward). Check for fan-out.
                // If card_map has (to, from) with ManyToOne:
                //   to is "many" side, from is "one" side.
                //   Walking from->to = one->many = FAN OUT
                if let Some(&card) = card_map.get(&(to.clone(), from.clone())) {
                    if card == Cardinality::ManyToOne {
                        fan_out = true;
                        break;
                    }
                }
            }
        }

        if fan_out {
            return false;
        }

        // If we get here for any met_table, the dimension is reachable
        return true;
    }

    // No metric source table found a valid path -- dimension not reachable
    met_tables.is_empty() // Edge case: derived metric with no source tables
}
```

**IMPORTANT:** The fan trap direction logic is subtle and must match `check_fan_traps` in expand.rs exactly. The executor MUST read the existing `check_path_up` and `check_path_down` functions in expand.rs (lines 1145-1200+) and replicate their direction semantics. The key insight:
- Edges in the graph go from FK table (from_alias) to PK table (table/to_alias)
- `card_map` key is `(from_alias_lower, to_alias_lower)` where from_alias is the FK side
- ManyToOne: from_alias is "many", to_alias is "one"
- Fan-out = traversing from "one" to "many" direction
- Safe = traversing from "many" to "one" direction (aggregation reduces)

**4. src/parse.rs -- No DdlKind variant needed:**

The FOR METRIC form is handled inside the ShowDimensions rewrite handler (see above). No new DdlKind variant required.

However, add to DDL_PREFIXES for near-miss detection:
```rust
"show semantic dimensions for metric",
```

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

**6. src/lib.rs -- Register the table function:**

```rust
// SHOW SEMANTIC DIMENSIONS FOR METRIC
con.register_table_function_with_extra_info::<ShowDimensionsForMetricVTab, _>(
    "show_semantic_dimensions_for_metric",
    &catalog_state,
)?;
```

Add import:
```rust
use ddl::show_dims_for_metric::ShowDimensionsForMetricVTab;
```
  </action>
  <verify>
    <automated>cargo test 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - src/ddl/show_dims_for_metric.rs exists and contains `pub struct ShowDimensionsForMetricVTab`
    - src/ddl/show_dims_for_metric.rs contains `ancestors_to_root` usage (imported from expand)
    - src/ddl/show_dims_for_metric.rs contains `collect_derived_metric_source_tables` usage (imported from expand)
    - src/ddl/show_dims_for_metric.rs contains `RelationshipGraph::from_definition`
    - src/expand.rs contains `pub(crate) fn ancestors_to_root` (visibility change)
    - src/expand.rs contains `pub(crate) fn collect_derived_metric_source_tables` (visibility change)
    - src/ddl/mod.rs contains `pub mod show_dims_for_metric;`
    - src/lib.rs contains `"show_semantic_dimensions_for_metric"`
    - src/parse.rs DDL_PREFIXES contains `"show semantic dimensions for metric"`
    - `cargo test` exits 0
  </acceptance_criteria>
  <done>SHOW SEMANTIC DIMENSIONS FOR METRIC has fan-trap-aware filtering VTab, parser rewrite integration, and compiles successfully</done>
</task>

<task type="auto">
  <name>Task 2: Add sqllogictest for SHOW DIMS FOR METRIC and run full test suite</name>
  <files>test/sql/phase34_1_show_dims_for_metric.test</files>
  <read_first>
    - src/ddl/show_dims_for_metric.rs (verify the implementation)
    - test/sql/phase31_fan_trap.test (existing fan trap test setup -- reusable table patterns)
    - test/sql/phase30_derived_metrics.test (derived metric patterns)
    - test/sql/phase34_1_show_commands.test (consistent test style from Plan 02)
  </read_first>
  <action>
Create `test/sql/phase34_1_show_dims_for_metric.test` with test cases covering:

**Test 1: Single-table view (all dimensions reachable)**
```
# Phase 34.1: SHOW SEMANTIC DIMENSIONS FOR METRIC

require semantic_views

# --- Setup: single-table view (all dims reachable) ---
statement ok
CREATE TABLE p34fm_sales (id INTEGER PRIMARY KEY, product VARCHAR, amount DECIMAL(10,2), region VARCHAR);

statement ok
CREATE SEMANTIC VIEW simple_sv AS
  TABLES (
    s AS p34fm_sales PRIMARY KEY (id)
  )
  DIMENSIONS (
    product := s.product TYPE VARCHAR,
    region := s.region TYPE VARCHAR
  )
  METRICS (
    total := SUM(s.amount) TYPE DECIMAL(10,2)
  )

query TTTTT rowsort
SHOW SEMANTIC DIMENSIONS IN simple_sv FOR METRIC total
----
simple_sv	product	s.product	(empty)	VARCHAR
simple_sv	region	s.region	(empty)	VARCHAR
```

**Test 2: Multi-table view with fan trap filtering**

Set up a star schema where:
- `orders` is the fact table (base)
- `customers` is a dimension table (orders -> customers via customer_id, ManyToOne)
- `line_items` is a detail table (line_items -> orders via order_id, ManyToOne)

A metric on `line_items` should see `line_items` dimensions but NOT `customers` dimensions (traversing from line_items to orders to customers would require going through orders which is the "one" side of line_items->orders, then orders->customers is safe. Actually, let me reconsider...

The correct test: a metric on a parent table and a dimension on a child table. If the metric is on `orders` and a dimension is on `line_items`, querying the metric with the line_items dimension would fan out (one order -> many line items). So that dimension should be EXCLUDED.

```
# --- Setup: multi-table view with fan trap ---
statement ok
CREATE TABLE p34fm_customers (id INTEGER PRIMARY KEY, name VARCHAR, country VARCHAR);

statement ok
CREATE TABLE p34fm_orders (id INTEGER PRIMARY KEY, customer_id INTEGER, total DECIMAL(10,2));

statement ok
CREATE TABLE p34fm_line_items (id INTEGER PRIMARY KEY, order_id INTEGER, qty INTEGER, price DECIMAL(10,2));

statement ok
CREATE SEMANTIC VIEW star_sv AS
  TABLES (
    c AS p34fm_customers PRIMARY KEY (id),
    o AS p34fm_orders PRIMARY KEY (id),
    li AS p34fm_line_items PRIMARY KEY (id)
  )
  RELATIONSHIPS (
    order_customer AS o(customer_id) REFERENCES c,
    item_order AS li(order_id) REFERENCES o
  )
  DIMENSIONS (
    customer_name := c.name SOURCE c TYPE VARCHAR,
    customer_country := c.country SOURCE c TYPE VARCHAR,
    item_qty := li.qty SOURCE li TYPE INTEGER
  )
  METRICS (
    order_total := SUM(o.total) SOURCE o TYPE DECIMAL(10,2),
    line_item_sum := SUM(li.price * li.qty) SOURCE li TYPE DECIMAL(10,2)
  )
```

For metric `order_total` (source: o):
- `customer_name` (source: c): o->c is ManyToOne, walking metric(o)->dim(c) goes from many to one = safe. INCLUDED.
- `customer_country` (source: c): same path. INCLUDED.
- `item_qty` (source: li): li->o is ManyToOne. Walking from o to li means going from "one" to "many" = fan-out. EXCLUDED.

```
# Test 2: order_total metric -- should see customer dims but NOT line_item dims
query TTTTT rowsort
SHOW SEMANTIC DIMENSIONS IN star_sv FOR METRIC order_total
----
star_sv	customer_country	c.country	c	VARCHAR
star_sv	customer_name	c.name	c	VARCHAR
```

For metric `line_item_sum` (source: li):
- `customer_name` (source: c): path is li->o->c. li->o is ManyToOne (safe, going up), o->c is ManyToOne (safe, going up). INCLUDED.
- `customer_country` (source: c): same path. INCLUDED.
- `item_qty` (source: li): same table. INCLUDED.

```
# Test 3: line_item_sum metric -- should see all dims (no fan-out going up)
query TTTTT rowsort
SHOW SEMANTIC DIMENSIONS IN star_sv FOR METRIC line_item_sum
----
star_sv	customer_country	c.country	c	VARCHAR
star_sv	customer_name	c.name	c	VARCHAR
star_sv	item_qty	li.qty	li	INTEGER
```

**Test 4: Non-existent metric**
```
# Test 4: non-existent metric
statement error
SHOW SEMANTIC DIMENSIONS IN star_sv FOR METRIC nonexistent
```

**Test 5: Non-existent view**
```
# Test 5: non-existent view
statement error
SHOW SEMANTIC DIMENSIONS IN nonexistent_sv FOR METRIC something
```

**Test 6: Derived metric**

Set up a derived metric that references a base metric. The derived metric should inherit its source table transitively.

```
# --- Setup: derived metric ---
statement ok
DROP SEMANTIC VIEW star_sv

statement ok
CREATE SEMANTIC VIEW derived_sv AS
  TABLES (
    c AS p34fm_customers PRIMARY KEY (id),
    o AS p34fm_orders PRIMARY KEY (id)
  )
  RELATIONSHIPS (
    order_customer AS o(customer_id) REFERENCES c
  )
  DIMENSIONS (
    customer_name := c.name SOURCE c TYPE VARCHAR
  )
  METRICS (
    order_total := SUM(o.total) SOURCE o TYPE DECIMAL(10,2),
    double_total := order_total * 2
  )

# double_total is derived from order_total (source: o). Same reachability as order_total.
query TTTTT rowsort
SHOW SEMANTIC DIMENSIONS IN derived_sv FOR METRIC double_total
----
derived_sv	customer_name	c.name	c	VARCHAR
```

**Test 7: Case insensitivity**
```
query TTTTT rowsort
show semantic dimensions in derived_sv for metric order_total
----
derived_sv	customer_name	c.name	c	VARCHAR
```

**Cleanup:**
```
# --- Cleanup ---
statement ok
DROP SEMANTIC VIEW simple_sv

statement ok
DROP SEMANTIC VIEW derived_sv

statement ok
DROP TABLE p34fm_line_items

statement ok
DROP TABLE p34fm_orders

statement ok
DROP TABLE p34fm_customers

statement ok
DROP TABLE p34fm_sales
```

**After creating the test file, run the full test suite:**
```bash
just build && just test-all
```

This is the phase quality gate per CLAUDE.md.
  </action>
  <verify>
    <automated>just build && just test-all 2>&1 | tail -15</automated>
  </verify>
  <acceptance_criteria>
    - test/sql/phase34_1_show_dims_for_metric.test exists
    - test/sql/phase34_1_show_dims_for_metric.test contains `SHOW SEMANTIC DIMENSIONS IN simple_sv FOR METRIC total`
    - test/sql/phase34_1_show_dims_for_metric.test contains `SHOW SEMANTIC DIMENSIONS IN star_sv FOR METRIC order_total`
    - test/sql/phase34_1_show_dims_for_metric.test contains `SHOW SEMANTIC DIMENSIONS IN star_sv FOR METRIC line_item_sum`
    - test/sql/phase34_1_show_dims_for_metric.test contains `SHOW SEMANTIC DIMENSIONS IN derived_sv FOR METRIC double_total`
    - test/sql/phase34_1_show_dims_for_metric.test contains `statement error` for nonexistent metric and nonexistent view
    - `just test-all` exits 0
  </acceptance_criteria>
  <done>SHOW DIMS FOR METRIC has sqllogictest coverage for: single-table, fan-trap-filtered multi-table, all-reachable multi-table, non-existent metric/view errors, derived metric, case insensitivity. Full test suite (`just test-all`) passes.</done>
</task>

</tasks>

<verification>
- `cargo test` passes (all unit tests)
- `just build && just test-sql` passes (all sqllogictests)
- `just test-all` passes (full quality gate)
- SHOW SEMANTIC DIMENSIONS IN view FOR METRIC metric returns only fan-trap-safe dimensions
- Derived metrics resolve source tables transitively
- Base table dimensions are always included
</verification>

<success_criteria>
- show_dims_for_metric.rs VTab module implements fan-trap-aware filtering
- Fan trap direction logic matches expand.rs check_fan_traps semantics exactly
- Derived metric source table resolution reuses collect_derived_metric_source_tables
- sqllogictest covers 7 test scenarios including fan trap filtering
- `just test-all` passes as the phase quality gate
</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-03-SUMMARY.md`
</output>
