---
phase: 12-explain-typed-output
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/model.rs
autonomous: true
requirements:
  - OUT-01

must_haves:
  truths:
    - "SemanticViewDefinition carries an `output_type` field on each Dimension and Metric for user-declared column types"
    - "SemanticViewDefinition carries parallel `column_type_names: Vec<String>` and `column_types_inferred: Vec<u32>` fields for DDL-time name→type storage"
    - "Old catalog JSON without `output_type` or `column_types_inferred` deserializes without error (backward compat)"
    - "Unit tests for both new fields and backward compat round-trip pass under `cargo test`"
  artifacts:
    - path: "src/model.rs"
      provides: "SemanticViewDefinition with output_type + column_types_inferred fields"
      contains: "column_types_inferred"
  key_links:
    - from: "src/model.rs"
      to: "src/ddl/define.rs"
      via: "DDL-time inference writes column_types_inferred into parsed SemanticViewDefinition before catalog serialize"
      pattern: "column_types_inferred"
    - from: "src/model.rs"
      to: "src/query/table_function.rs"
      via: "bind() reads column_types_inferred to declare typed output columns"
      pattern: "column_types_inferred"
---

<objective>
Add `output_type: Option<String>` to `Dimension` and `Metric` structs, and `column_types_inferred: Vec<u32>` to `SemanticViewDefinition`. These are the data contracts that Plans 02 and 03 implement against.

Purpose: Establish the typed output data model before wiring DDL-time inference (Plan 02) and typed bind/func (Plan 03).
Output: Updated `src/model.rs` with new serde-defaulted fields and 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/12-explain-typed-output/12-RESEARCH.md

<interfaces>
<!-- Current model.rs key structs — Plan 01 adds fields to these. -->

From src/model.rs:
```rust
pub struct Dimension {
    pub name: String,
    pub expr: String,
    #[serde(default)]
    pub source_table: Option<String>,
    #[serde(default, rename = "type")]
    pub dim_type: Option<String>,
    #[serde(default)]
    pub granularity: Option<String>,
    // Phase 12: ADD output_type: Option<String> here
}

pub struct Metric {
    pub name: String,
    pub expr: String,
    #[serde(default)]
    pub source_table: Option<String>,
    // Phase 12: ADD output_type: Option<String> here
}

pub struct SemanticViewDefinition {
    pub base_table: String,
    pub tables: Vec<TableRef>,
    pub dimensions: Vec<Dimension>,
    pub metrics: Vec<Metric>,
    pub filters: Vec<String>,
    pub joins: Vec<Join>,
    pub facts: Vec<Fact>,
    // Phase 12: ADD column_types_inferred: Vec<u32> here
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add output_type + column_types_inferred fields to model.rs</name>
  <files>src/model.rs</files>
  <behavior>
    - New field `output_type: Option<String>` on `Dimension` with `#[serde(default)]` — absent from old JSON deserializes to None
    - New field `output_type: Option<String>` on `Metric` with `#[serde(default)]` — absent from old JSON deserializes to None
    - New field `column_type_names: Vec<String>` on `SemanticViewDefinition` with `#[serde(default)]` — absent from old JSON deserializes to vec![]
    - New field `column_types_inferred: Vec<u32>` on `SemanticViewDefinition` with `#[serde(default)]` — absent from old JSON deserializes to vec![]
    - `column_type_names` and `column_types_inferred` are parallel vecs: `column_type_names[i]` is the column name for `column_types_inferred[i]`; both are always the same length
    - Round-trip test: SemanticViewDefinition with all new fields set serializes and deserializes correctly
    - Backward compat test: old JSON without new fields still deserializes (serde default kicks in)
    - `output_type` on Dimension: `Some("BIGINT")` round-trips correctly
    - `output_type` on Metric: `Some("DOUBLE")` round-trips correctly
    - `column_type_names` with `["region", "revenue"]` and `column_types_inferred` with `[17u32, 20u32]` round-trips correctly (17 = VARCHAR, 20 = BIGINT)
  </behavior>
  <action>
    Add the following fields to the structs in `src/model.rs`:

    To `Dimension` struct (after `granularity` field):
    ```rust
    /// Optional user-declared output type for this dimension column.
    /// When set, the generated SQL wraps the expression in `CAST(expr AS <type>)`
    /// AND declares the output column as this type in `bind()`.
    /// If None, the inferred or fallback type is used.
    #[serde(default)]
    pub output_type: Option<String>,
    ```

    To `Metric` struct (after `source_table` field):
    ```rust
    /// Optional user-declared output type for this metric column.
    /// When set, the generated SQL wraps the expression in `CAST(expr AS <type>)`
    /// AND declares the output column as this type in `bind()`.
    /// If None, the inferred or fallback type is used.
    #[serde(default)]
    pub output_type: Option<String>,
    ```

    To `SemanticViewDefinition` struct (after `facts` field):
    ```rust
    /// Column names from DDL-time LIMIT 0 inference, parallel to `column_types_inferred`.
    /// Populated by `create_semantic_view` invoke(). Empty = no inference ran.
    /// Used by bind() to build a name→type map for subquery column lookups.
    #[serde(default)]
    pub column_type_names: Vec<String>,

    /// DDL-time inferred column types stored as `ffi::duckdb_type` (u32) values.
    /// Populated by `create_semantic_view` invoke() after running LIMIT 0.
    /// Parallel to `column_type_names`: column_type_names[i] ↔ column_types_inferred[i].
    /// Empty vec = no inference ran (in-memory DB or inference failed) → VARCHAR fallback.
    /// bind() builds a name→type HashMap from both vecs to look up requested columns by name.
    #[serde(default)]
    pub column_types_inferred: Vec<u32>,
    ```

    Add unit tests in the `mod tests` block (in a new `mod phase12_model_tests` sub-module):
    - `output_type_on_dimension_roundtrips`: creates Dimension with `output_type: Some("BIGINT")`, serializes, deserializes, asserts field preserved
    - `output_type_on_metric_roundtrips`: same for Metric with `Some("DOUBLE")`
    - `column_types_inferred_roundtrips`: creates SemanticViewDefinition with `column_types_inferred: vec![20, 4]`, serializes, deserializes, asserts field preserved
    - `old_json_without_output_type_deserializes`: parse a JSON string without `output_type` fields — must succeed with None values
    - `old_json_without_column_types_inferred_deserializes`: parse a JSON string without `column_type_names` or `column_types_inferred` — must succeed with empty vecs for both

    Do NOT update `from_json()` validation logic — `output_type` is unconstrained at validation time (any string is accepted; enforcement is at codegen time).

    Run `cargo test` (no extension feature) to verify all existing + new tests pass.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test 2>&1 | tail -20</automated>
  </verify>
  <done>
    `cargo test` passes. `src/model.rs` contains `output_type` on Dimension and Metric, `column_types_inferred` on SemanticViewDefinition, all with `#[serde(default)]`. Five new unit tests in `mod phase12_model_tests` pass.
  </done>
</task>

</tasks>

<verification>
Run `cargo test` from project root. All tests must pass with no new warnings.

Check `grep -n "output_type\|column_types_inferred" src/model.rs` returns hits on Dimension, Metric, and SemanticViewDefinition.
</verification>

<success_criteria>
`src/model.rs` exports `output_type: Option<String>` on `Dimension` and `Metric`, and `column_types_inferred: Vec<u32>` on `SemanticViewDefinition`, all with `#[serde(default)]`. `cargo test` passes. Old JSON without these fields deserializes without error.
</success_criteria>

<output>
After completion, create `.planning/phases/12-explain-typed-output/12-01-SUMMARY.md`
</output>
