---
phase: 41-describe-rewrite
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/ddl/describe.rs
  - test/sql/phase41_describe.test
autonomous: true
requirements: [DESC-01, DESC-02, DESC-03, DESC-04, DESC-05, DESC-06, DESC-07]

must_haves:
  truths:
    - "DESCRIBE SEMANTIC VIEW returns rows with 5 columns: object_kind, object_name, parent_entity, property, property_value"
    - "TABLE objects emit BASE_TABLE_DATABASE_NAME, BASE_TABLE_SCHEMA_NAME, BASE_TABLE_NAME, and PRIMARY_KEY (when non-empty) properties"
    - "RELATIONSHIP objects emit TABLE, REF_TABLE, FOREIGN_KEY, REF_KEY properties with JSON array format"
    - "DIMENSION objects emit TABLE, EXPRESSION, DATA_TYPE properties with parent_entity set to source table alias"
    - "FACT objects emit TABLE, EXPRESSION, DATA_TYPE properties with parent_entity set to source table alias"
    - "METRIC objects (source_table Some) emit TABLE, EXPRESSION, DATA_TYPE properties"
    - "DERIVED_METRIC objects (source_table None) emit EXPRESSION, DATA_TYPE only (no TABLE), with empty parent_entity"
  artifacts:
    - path: "src/ddl/describe.rs"
      provides: "Property-per-row DESCRIBE VTab implementation"
      contains: "struct DescribeRow"
    - path: "test/sql/phase41_describe.test"
      provides: "Comprehensive DESCRIBE sqllogictest coverage"
      contains: "object_kind"
  key_links:
    - from: "src/ddl/describe.rs"
      to: "crate::model::SemanticViewDefinition"
      via: "from_json parsing in bind()"
      pattern: "SemanticViewDefinition::from_json"
    - from: "src/ddl/describe.rs"
      to: "crate::catalog::CatalogState"
      via: "extra_info in bind()"
      pattern: "get_extra_info::<CatalogState>"
---

<objective>
Rewrite describe.rs from single-row JSON-blob format to Snowflake-aligned property-per-row format and create comprehensive sqllogictest coverage for all 6 object kinds.

Purpose: Align DESCRIBE SEMANTIC VIEW output with Snowflake specification (DESC-01 through DESC-07). This is a complete VTab rewrite, not an incremental change.
Output: Rewritten describe.rs with DescribeRow struct, new phase41_describe.test with full coverage.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/41-describe-rewrite/41-RESEARCH.md

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

From src/model.rs:
```rust
pub struct TableRef {
    pub alias: String,
    pub table: String,
    pub pk_columns: Vec<String>,       // serde(default), may be empty
    pub unique_constraints: Vec<Vec<String>>,
}

pub struct Dimension {
    pub name: String,
    pub expr: String,
    pub source_table: Option<String>,  // None = base table
    pub output_type: Option<String>,   // None = empty string in output
}

pub struct Metric {
    pub name: String,
    pub expr: String,
    pub source_table: Option<String>,  // None = DERIVED_METRIC, Some = METRIC
    pub output_type: Option<String>,
    pub using_relationships: Vec<String>,
}

pub struct Fact {
    pub name: String,
    pub expr: String,
    pub source_table: Option<String>,
    pub output_type: Option<String>,
}

pub struct Join {
    pub table: String,           // target table alias
    pub from_alias: String,      // source table alias (FK side)
    pub fk_columns: Vec<String>, // FK column names
    pub ref_columns: Vec<String>,// referenced PK/UNIQUE columns
    pub name: Option<String>,    // relationship name (skip if None)
    // ... other legacy fields not needed for DESCRIBE
}

pub struct SemanticViewDefinition {
    pub base_table: String,
    pub tables: Vec<TableRef>,
    pub dimensions: Vec<Dimension>,
    pub metrics: Vec<Metric>,
    pub joins: Vec<Join>,
    pub facts: Vec<Fact>,
    pub created_on: Option<String>,
    pub database_name: Option<String>,
    pub schema_name: Option<String>,
    // ... other fields not needed for DESCRIBE
}

impl SemanticViewDefinition {
    pub fn from_json(name: &str, json: &str) -> Result<Self, String>;
}
```

From src/ddl/show_dims.rs (reference VTab pattern):
```rust
// Row struct -> BindData with Vec<Row> -> AtomicBool InitData -> emit in func()
struct ShowDimRow { /* fields */ }
pub struct ShowDimsBindData { rows: Vec<ShowDimRow> }
pub struct ShowDimsInitData { done: AtomicBool }
// bind: parse definition, collect rows, sort, return BindData
// func: swap done flag, emit all rows via flat_vector inserts, set_len
```

From src/catalog.rs:
```rust
pub type CatalogState = Arc<RwLock<HashMap<String, String>>>;
// Accessed via bind.get_extra_info::<CatalogState>()
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Rewrite describe.rs to property-per-row format</name>
  <files>src/ddl/describe.rs</files>
  <read_first>
    - src/ddl/describe.rs (current implementation to replace)
    - src/ddl/show_dims.rs (reference VTab pattern for multi-row emission)
    - src/model.rs (SemanticViewDefinition, TableRef, Dimension, Metric, Fact, Join structs)
    - .planning/phases/41-describe-rewrite/41-RESEARCH.md (full specification)
  </read_first>
  <action>
    Complete rewrite of src/ddl/describe.rs. Replace the entire file content following the show_dims.rs VTab pattern.

    **Struct definitions:**

    1. `DescribeRow` struct with 5 String fields: `object_kind`, `object_name`, `parent_entity`, `property`, `property_value`
    2. `DescribeBindData` with `rows: Vec<DescribeRow>` (replaces old 6 String fields)
    3. `DescribeInitData` with `done: AtomicBool` (unchanged pattern)
    4. `unsafe impl Send + Sync` for both BindData and InitData (same as current)

    **Helper functions:**

    5. `format_json_array(items: &[String]) -> String` -- formats as `["col1","col2"]` with NO spaces after commas. Use `format!("\"{}\"", s)` for each item, join with `","`, wrap in `[]`.

    6. `collect_table_rows(def: &SemanticViewDefinition, rows: &mut Vec<DescribeRow>)`:
       - Get `db_name = def.database_name.clone().unwrap_or_default()` and `sch_name = def.schema_name.clone().unwrap_or_default()`
       - If `def.tables` is empty, emit one TABLE block using `def.base_table` as both alias and table name with empty pk_columns (no PRIMARY_KEY row). This handles legacy pre-Phase-24 definitions.
       - For each `table` in `def.tables`: emit 3-4 property rows:
         - `(TABLE, alias, "", BASE_TABLE_DATABASE_NAME, db_name)`
         - `(TABLE, alias, "", BASE_TABLE_SCHEMA_NAME, sch_name)`
         - `(TABLE, alias, "", BASE_TABLE_NAME, table.table)`
         - `(TABLE, alias, "", PRIMARY_KEY, format_json_array(&table.pk_columns))` -- ONLY if `!table.pk_columns.is_empty()`

    7. `collect_relationship_rows(def: &SemanticViewDefinition, rows: &mut Vec<DescribeRow>)`:
       - For each `join` in `def.joins`: skip if `join.name.is_none()` (unnamed legacy joins per Pitfall 4)
       - Also skip if `join.from_alias.is_empty()` (legacy joins per Open Question 3)
       - `parent_entity = join.from_alias.clone()`
       - `object_name = join.name.clone().unwrap()` (safe after None check)
       - Emit 4 property rows: TABLE (value=from_alias), REF_TABLE (value=join.table), FOREIGN_KEY (format_json_array of fk_columns), REF_KEY (format_json_array of ref_columns)

    8. `collect_fact_rows(def: &SemanticViewDefinition, base_alias: &str, rows: &mut Vec<DescribeRow>)`:
       - For each `fact` in `def.facts`:
         - `parent = fact.source_table.clone().unwrap_or_else(|| base_alias.to_string())`
         - Emit 3 rows: TABLE (value=parent), EXPRESSION (value=fact.expr), DATA_TYPE (value=fact.output_type.clone().unwrap_or_default())
         - object_kind = "FACT", object_name = fact.name

    9. `collect_dimension_rows(def: &SemanticViewDefinition, base_alias: &str, rows: &mut Vec<DescribeRow>)`:
       - Same pattern as facts but object_kind = "DIMENSION"
       - `parent = dim.source_table.clone().unwrap_or_else(|| base_alias.to_string())`
       - Emit 3 rows: TABLE, EXPRESSION, DATA_TYPE

    10. `collect_metric_rows(def: &SemanticViewDefinition, base_alias: &str, rows: &mut Vec<DescribeRow>)`:
        - For each `metric` in `def.metrics`:
          - `is_derived = metric.source_table.is_none()`
          - If derived: object_kind = "DERIVED_METRIC", parent_entity = "" (empty), emit 2 rows: EXPRESSION, DATA_TYPE (NO TABLE row)
          - If not derived: object_kind = "METRIC", parent_entity = source_table value, emit 3 rows: TABLE, EXPRESSION, DATA_TYPE

    **VTab implementation (DescribeSemanticViewVTab):**

    11. `bind()`:
        - Declare 5 output columns: object_kind, object_name, parent_entity, property, property_value (all VARCHAR)
        - Read view name from parameter 0
        - Look up JSON in CatalogState via `get_extra_info::<CatalogState>()`
        - Parse into `SemanticViewDefinition::from_json()`
        - Compute `base_alias = def.tables.first().map(|t| t.alias.clone()).unwrap_or_else(|| def.base_table.clone())`
        - Collect rows in order: tables, relationships, facts, dimensions, metrics (this matches natural definition reading order per Open Question 1)
        - Return `DescribeBindData { rows }`

    12. `init()`: Return `DescribeInitData { done: AtomicBool::new(false) }` (unchanged)

    13. `func()`:
        - Same AtomicBool done-flag pattern as show_dims.rs
        - Get 5 flat vectors (indices 0-4)
        - Iterate rows, insert each field at index i
        - `output.set_len(n)`

    14. `parameters()`: `Some(vec![LogicalTypeHandle::from(LogicalTypeId::Varchar)])` (unchanged)

    **Imports needed:**
    - `use std::sync::atomic::{AtomicBool, Ordering};`
    - `use duckdb::{core::{DataChunkHandle, Inserter, LogicalTypeHandle, LogicalTypeId}, vtab::{BindInfo, InitInfo, TableFunctionInfo, VTab}};`
    - `use crate::catalog::CatalogState;`
    - `use crate::model::SemanticViewDefinition;`

    **Key decisions (from research):**
    - Empty string for parent_entity when Snowflake uses NULL (VARCHAR column, matches show_dims.rs pattern)
    - Empty string for DATA_TYPE when output_type is None (matches Phase 40 SHOW behavior)
    - Skip unnamed/legacy joins for RELATIONSHIP rows
    - Only emit PRIMARY_KEY row when pk_columns is non-empty
    - Row ordering: tables, relationships, facts, dimensions, metrics (definition order)
  </action>
  <verify>
    <automated>cargo test -- describe 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - File src/ddl/describe.rs contains `struct DescribeRow` with fields: object_kind, object_name, parent_entity, property, property_value
    - File contains `fn format_json_array` that produces `["col1","col2"]` format
    - File contains `fn collect_table_rows`, `fn collect_relationship_rows`, `fn collect_fact_rows`, `fn collect_dimension_rows`, `fn collect_metric_rows`
    - DescribeBindData has field `rows: Vec<DescribeRow>`
    - bind() declares exactly 5 output columns named object_kind, object_name, parent_entity, property, property_value
    - bind() calls SemanticViewDefinition::from_json (not raw serde_json::Value parsing)
    - func() uses flat_vector(0) through flat_vector(4) for 5 columns
    - Metric/DerivedMetric split uses `metric.source_table.is_none()` check
    - PRIMARY_KEY row only emitted when `!table.pk_columns.is_empty()`
    - `cargo test -- describe` compiles without error (tests may fail until plan 02 updates test expectations)
  </acceptance_criteria>
  <done>describe.rs fully rewritten with property-per-row VTab, compiles cleanly, follows show_dims.rs pattern</done>
</task>

<task type="auto">
  <name>Task 2: Create comprehensive phase41_describe.test</name>
  <files>test/sql/phase41_describe.test</files>
  <read_first>
    - src/ddl/describe.rs (the just-rewritten file from Task 1)
    - test/sql/phase20_extended_ddl.test (reference for test structure and table setup patterns)
    - test/sql/phase29_facts.test (reference for facts + relationships setup)
    - test/sql/phase30_derived_metrics.test (reference for derived metrics setup)
    - .planning/phases/41-describe-rewrite/41-RESEARCH.md (specification and expected output)
  </read_first>
  <action>
    Create a new comprehensive sqllogictest file at test/sql/phase41_describe.test that covers ALL 7 DESC requirements.

    **File header:**
    ```
    # Phase 41: DESCRIBE SEMANTIC VIEW — Snowflake-aligned property-per-row format
    # Tests: DESC-01 through DESC-07
    ```

    **Test structure:**

    **Setup tables** (reusable across tests):
    ```sql
    statement ok
    CREATE TABLE p41_customers (id INTEGER, name VARCHAR(50), email VARCHAR(100), country VARCHAR(30));

    statement ok
    CREATE TABLE p41_orders (id INTEGER, customer_id INTEGER, region VARCHAR(20), amount DECIMAL(10,2), order_date DATE);

    statement ok
    CREATE TABLE p41_line_items (id INTEGER, order_id INTEGER, product VARCHAR, quantity INTEGER, price DECIMAL(10,2));
    ```

    **Test 1: Simple view — TABLE + DIMENSION + METRIC (DESC-01, DESC-02, DESC-04, DESC-06)**

    Create a simple single-table semantic view:
    ```sql
    CREATE SEMANTIC VIEW p41_simple AS
      TABLES (o AS p41_orders PRIMARY KEY (id))
      DIMENSIONS (o.region AS region)
      METRICS (o.total AS SUM(o.amount))
    ```

    Assert with `query TTTTT` (5 columns, no rowsort — definition order):
    Expected rows (exact order):
    ```
    TABLE	o	(empty)	BASE_TABLE_DATABASE_NAME	(empty or memory)
    TABLE	o	(empty)	BASE_TABLE_SCHEMA_NAME	(empty or main)
    TABLE	o	(empty)	BASE_TABLE_NAME	p41_orders
    TABLE	o	(empty)	PRIMARY_KEY	["id"]
    DIMENSION	region	o	TABLE	o
    DIMENSION	region	o	EXPRESSION	o.region
    DIMENSION	region	o	DATA_TYPE	(empty)
    METRIC	total	o	TABLE	o
    METRIC	total	o	EXPRESSION	SUM(o.amount)
    METRIC	total	o	DATA_TYPE	(empty)
    ```

    NOTE: The database_name and schema_name values come from the connection context at define time. In sqllogictest these will typically be `memory` and `main` respectively (since in-memory DuckDB uses `memory` as database name and `main` as default schema). However, since the metadata capture was added in Phase 39 and uses catalog_conn, verify what values actually appear. Use `query TTTTT rowsort` if needed for stability, but prefer exact order to verify definition-order emission.

    IMPORTANT: Since metadata values (database_name, schema_name) depend on the DuckDB connection context, use the actual values that DuckDB in-memory produces. Test by running `SELECT current_database(), current_schema()` in a sqllogictest to see. These are typically empty strings since the metadata capture via catalog_conn may not resolve for in-memory test databases. Check what Phase 40 SHOW tests use for reference.

    **Test 2: Multi-table view with relationships (DESC-03)**

    Create a view with tables, relationships, dimensions, and metrics across tables:
    ```sql
    CREATE SEMANTIC VIEW p41_multi AS
      TABLES (
        o AS p41_orders PRIMARY KEY (id),
        c AS p41_customers PRIMARY KEY (id)
      )
      RELATIONSHIPS (
        order_to_customer AS o(customer_id) REFERENCES c
      )
      DIMENSIONS (
        o.region AS region,
        c.name AS customer_name
      )
      METRICS (
        o.total_revenue AS SUM(o.amount)
      )
    ```

    Assert: TABLE rows for o (4 props) + TABLE rows for c (4 props) + RELATIONSHIP rows for order_to_customer (4 props: TABLE=o, REF_TABLE=c, FOREIGN_KEY=["customer_id"], REF_KEY=["id"]) + DIMENSION rows (region 3 props, customer_name 3 props) + METRIC rows (total_revenue 3 props). Total: 4+4+4+3+3+3 = 21 rows.

    **Test 3: Facts (DESC-05)**

    Create a view with facts:
    ```sql
    CREATE SEMANTIC VIEW p41_facts AS
      TABLES (
        o AS p41_orders PRIMARY KEY (id),
        li AS p41_line_items PRIMARY KEY (id)
      )
      RELATIONSHIPS (
        li_to_order AS li(order_id) REFERENCES o
      )
      FACTS (
        li.net_price AS li.price * li.quantity
      )
      DIMENSIONS (
        o.region AS region
      )
      METRICS (
        o.total_net AS SUM(li.net_price)
      )
    ```

    Assert: FACT rows for net_price with parent_entity=li, 3 properties (TABLE, EXPRESSION, DATA_TYPE).

    **Test 4: Derived metrics (DESC-07)**

    Create a view with both regular and derived metrics:
    ```sql
    CREATE SEMANTIC VIEW p41_derived AS
      TABLES (o AS p41_orders PRIMARY KEY (id))
      DIMENSIONS (o.region AS region)
      METRICS (
        o.revenue AS SUM(o.amount),
        profit AS revenue * 0.3
      )
    ```

    Assert: revenue is METRIC (has TABLE row, parent_entity=o), profit is DERIVED_METRIC (NO TABLE row, parent_entity empty). Verify profit has only 2 property rows (EXPRESSION, DATA_TYPE) while revenue has 3 (TABLE, EXPRESSION, DATA_TYPE).

    **Test 5: Table without PRIMARY KEY (Pitfall 3)**

    Create a view with a table that has no PK:
    ```sql
    CREATE SEMANTIC VIEW p41_nopk AS
      TABLES (o AS p41_orders)
      DIMENSIONS (o.region AS region)
      METRICS (o.total AS SUM(o.amount))
    ```

    Assert: TABLE rows for o have only 3 properties (BASE_TABLE_DATABASE_NAME, BASE_TABLE_SCHEMA_NAME, BASE_TABLE_NAME) — no PRIMARY_KEY row.

    **Test 6: Error case — nonexistent view**
    ```sql
    statement error
    DESCRIBE SEMANTIC VIEW p41_nonexistent;
    ----
    does not exist
    ```

    **Test 7: Case insensitivity**
    ```sql
    query TTTTT
    describe semantic view p41_simple;
    ```
    Assert same results as Test 1.

    **Test 8: COUNT(*) verification**
    Assert `SELECT COUNT(*) FROM describe_semantic_view('p41_simple')` returns the expected row count (10 for the simple view with PK, or appropriate count).

    **Cleanup:** DROP all created semantic views at the end.

    **CRITICAL implementation notes:**
    - Use `query TTTTT` (5 T's) for all DESCRIBE queries (5 VARCHAR columns)
    - For empty string values in expected output, use tab-separated values where the empty cell is just... empty (two consecutive tabs or a tab at start/end of line). In sqllogictest, empty VARCHAR renders as `(empty)` — check existing test patterns.
    - Actually in sqllogictest, empty strings render as literally empty (no text between tabs). Verify against Phase 40 test patterns for empty data_type values.
    - Do NOT use `rowsort` unless necessary — we want to verify definition-order emission.
  </action>
  <verify>
    <automated>just test-all 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - File test/sql/phase41_describe.test exists
    - File contains tests covering all 6 object_kind values: TABLE, RELATIONSHIP, DIMENSION, FACT, METRIC, DERIVED_METRIC
    - File contains `query TTTTT` (5 T's for 5 columns) for DESCRIBE assertions
    - File contains test for table without PRIMARY KEY (no PRIMARY_KEY row emitted)
    - File contains test for derived metric (no TABLE property row, empty parent_entity)
    - File contains test with RELATIONSHIP rows including FOREIGN_KEY and REF_KEY in JSON array format
    - File contains `statement error` test for nonexistent view with "does not exist"
    - File contains COUNT(*) verification test
    - `just test-all` passes (all existing tests + new test green)
  </acceptance_criteria>
  <done>Comprehensive phase41_describe.test exists and passes. All 7 DESC requirements have dedicated test coverage. `just test-all` is green.</done>
</task>

</tasks>

<verification>
1. `cargo test -- describe` passes (Rust unit tests related to describe)
2. `just build && just test-sql` passes (all sqllogictests including new phase41_describe.test)
3. `just test-all` passes (full quality gate)
4. New test file covers all 6 object_kind values with property assertions
5. DESCRIBE output has exactly 5 columns (not 6)
</verification>

<success_criteria>
- describe.rs completely rewritten with DescribeRow struct and property-per-row emission
- New phase41_describe.test comprehensively covers DESC-01 through DESC-07
- All existing tests still pass (no regressions from describe.rs rewrite — see Plan 02 for test updates)
- `just test-all` green
</success_criteria>

<output>
After completion, create `.planning/phases/41-describe-rewrite/41-01-SUMMARY.md`
</output>
