---
phase: 11.1-review-possible-ddl-and-query-syntax-options-and-bring-it-as-close-as-we-can-to-snowflake-semantic-views
plan: "02"
type: tdd
wave: 2
depends_on: ["01"]
files_modified:
  - src/ddl/parse_args.rs
  - src/ddl/mod.rs
autonomous: true
requirements:
  - DDL-03
  - DDL-04

must_haves:
  truths:
    - "parse_args.rs exists and is pub-used from ddl/mod.rs"
    - "parse_args::parse_define_args converts 6-tuple VScalar inputs into SemanticViewDefinition"
    - "Unit tests cover tables, relationships (with nested join_columns), dimensions, time_dimensions, metrics"
    - "Empty list inputs produce empty Vecs without panic"
    - "cargo test passes for parse_args tests"
  artifacts:
    - path: "src/ddl/parse_args.rs"
      provides: "parse_define_args function — STRUCT/LIST vector reading"
      exports:
        - "pub struct ParsedDefineArgs { pub name: String, pub def: SemanticViewDefinition }"
        - "pub fn parse_define_args(input: &mut DataChunkHandle) -> Result<ParsedDefineArgs, String>"
    - path: "src/ddl/mod.rs"
      provides: "pub mod parse_args re-export"
  key_links:
    - from: "src/ddl/parse_args.rs"
      to: "src/ddl/define.rs (Plan 04)"
      via: "parse_define_args called from DefineSemanticView::invoke"
---

<objective>
Build the STRUCT/LIST argument parser for the new define_semantic_view DDL signature using TDD.

Purpose: The new DDL takes 6 positional args (name, tables, relationships, dimensions, time_dimensions, metrics) — all typed as LIST(STRUCT). Reading nested STRUCTs and LISTs from a DataChunkHandle is the most complex part of the implementation. Isolating it in parse_args.rs with thorough unit tests prevents this complexity from polluting define.rs.

Output: src/ddl/parse_args.rs with parse_define_args function and unit test suite; mod.rs updated.
</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/phases/11.1-review-possible-ddl-and-query-syntax-options-and-bring-it-as-close-as-we-can-to-snowflake-semantic-views/11.1-CONTEXT.md
@.planning/phases/11.1-review-possible-ddl-and-query-syntax-options-and-bring-it-as-close-as-we-can-to-snowflake-semantic-views/11.1-RESEARCH.md
@.planning/phases/11.1-review-possible-ddl-and-query-syntax-options-and-bring-it-as-close-as-we-can-to-snowflake-semantic-views/11.1-01-SUMMARY.md

<interfaces>
<!-- Types from Plan 01 model.rs additions -->

```rust
// src/model.rs (from Plan 01)
pub struct TableRef { pub alias: String, pub table: String }
pub struct JoinColumn { pub from: String, pub to: String }
// Join now has: pub join_columns: Vec<JoinColumn>  (serde default)
// SemanticViewDefinition now has: pub tables: Vec<TableRef>  (serde default)
```

<!-- VScalar argument positions for new define_semantic_view -->
// arg 0: VARCHAR  — view name
// arg 1: LIST(STRUCT(alias VARCHAR, table VARCHAR))
// arg 2: LIST(STRUCT(from_table VARCHAR, to_table VARCHAR, join_columns LIST(STRUCT(from VARCHAR, to VARCHAR))))
// arg 3: LIST(STRUCT(name VARCHAR, expr VARCHAR, source_table VARCHAR))
// arg 4: LIST(STRUCT(name VARCHAR, expr VARCHAR, granularity VARCHAR))
// arg 5: LIST(STRUCT(name VARCHAR, expr VARCHAR, source_table VARCHAR))

<!-- duckdb-rs 1.4.4 LIST(STRUCT) reading pattern (from RESEARCH.md) -->
```rust
// Reading LIST(STRUCT(alias VARCHAR, table VARCHAR)) from column col_idx:
let list_vec = input.list_vector(col_idx);
let total_elems = list_vec.len();
let struct_child = list_vec.struct_child(total_elems);
let alias_fv = struct_child.child(0, total_elems);  // field 0 = alias
let table_fv = struct_child.child(1, total_elems);  // field 1 = table
let alias_data = alias_fv.as_slice_with_len::<duckdb_string_t>(total_elems);
let table_data = table_fv.as_slice_with_len::<duckdb_string_t>(total_elems);
for row in 0..input.len() {
    let (offset, len) = list_vec.get_entry(row);
    for i in offset..offset + len {
        let alias = DuckString::new(&mut { alias_data[i] }).as_str().to_string();
        // ...
    }
}
```

<!-- PITFALLS from RESEARCH.md -->
// Pitfall 2: ListVector `entries` field is PRIVATE — use get_entry(idx) -> (usize, usize)
// Pitfall 3: StructVector::child(idx) is 0-indexed by field declaration order
// Pitfall 5: Nested LIST in STRUCT — call inner_list_vec.len() for inner child count
// Pitfall 6: time_dimensions must set dim_type: Some("time".to_string()) + granularity on Dimension
// Pitfall 7: Check for NULL list inputs — use empty Vec if input len is 0
</interfaces>
</context>

<feature>
  <name>parse_define_args — STRUCT/LIST → SemanticViewDefinition parser</name>
  <files>src/ddl/parse_args.rs, src/ddl/mod.rs</files>
  <behavior>
    Input: `&mut DataChunkHandle` with 6 columns at row index 0 (VScalar processes one row at a time)

    Cases (unit tests):
    - parse_name: Column 0 VARCHAR "my_view" → name = "my_view"
    - parse_tables_single: Column 1 LIST with [{alias:"o", table:"orders"}] → tables = [TableRef { alias:"o", table:"orders" }]
    - parse_tables_empty: Column 1 empty list → tables = []
    - parse_relationships_with_join_columns: Column 2 LIST with [{from_table:"o", to_table:"c", join_columns:[{from:"customer_id", to:"id"}]}] → joins = [Join { table:"c", on:"", from_cols:[], join_columns:[JoinColumn{from:"customer_id",to:"id"}] }]
    - parse_relationships_empty: Column 2 empty list → joins = []
    - parse_dimensions: Column 3 LIST with [{name:"region",expr:"o.region",source_table:"o"}] → dimensions = [Dimension { name:"region", expr:"o.region", source_table:Some("o"), dim_type:None, granularity:None }]
    - parse_time_dimensions: Column 4 LIST with [{name:"order_date",expr:"o.created_at",granularity:"day"}] → dimensions includes Dimension { name:"order_date", ..., dim_type:Some("time"), granularity:Some("day") }
    - parse_metrics: Column 5 LIST with [{name:"revenue",expr:"sum(o.amount)",source_table:"o"}] → metrics = [Metric { name:"revenue", ... }]
    - all_empty_lists: Columns 1-5 all empty → valid SemanticViewDefinition with all Vecs empty, base_table set from first table's table field (or empty string if no tables)

    Note: Unit tests for parse_args.rs will be `#[cfg(test)]` using mock DataChunkHandle construction. Because duckdb-rs DataChunkHandle is not easily constructable in unit tests without a live DuckDB connection, the unit tests focus on the helper extraction functions (read_varchar_list_struct, etc.) using the verified logic from integration. The full parse_define_args function is tested via the integration test in Plan 05.

    IMPORTANT: Write tests for the pure helper functions that do not require DataChunkHandle. Extract helpers:
    - `fn read_str(data: &[duckdb_string_t], i: usize) -> String` — reads a DuckString
    - Document parse_define_args logic clearly so integration test in Plan 05 validates it end-to-end.
  </behavior>
  <implementation>
    Create src/ddl/parse_args.rs with:

    1. Imports:
    ```rust
    use duckdb::core::DataChunkHandle;
    use duckdb::types::DuckString;
    use libduckdb_sys::duckdb_string_t;
    use crate::model::{Dimension, JoinColumn, Join, Metric, SemanticViewDefinition, TableRef};
    ```

    2. `pub struct ParsedDefineArgs { pub name: String, pub def: SemanticViewDefinition }`

    3. `pub unsafe fn parse_define_args(input: &mut DataChunkHandle, row: usize) -> Result<ParsedDefineArgs, String>`

    For each column, follow the RESEARCH.md Pattern 2 for LIST(STRUCT) reading. Key implementation notes:

    **Column 0 — name (VARCHAR):**
    ```rust
    let name_col = input.flat_vector(0);
    let names = name_col.as_slice_with_len::<duckdb_string_t>(input.len());
    let name = DuckString::new(&mut { names[row] }).as_str().to_string();
    ```

    **Column 1 — tables LIST(STRUCT(alias, table)):**
    ```rust
    let tables_list = input.list_vector(1);
    let total_tables = tables_list.len();
    let mut tables: Vec<TableRef> = Vec::new();
    if total_tables > 0 {
        let struct_child = tables_list.struct_child(total_tables);
        let alias_fv = struct_child.child(0, total_tables);
        let table_fv = struct_child.child(1, total_tables);
        let alias_data = alias_fv.as_slice_with_len::<duckdb_string_t>(total_tables);
        let table_data = table_fv.as_slice_with_len::<duckdb_string_t>(total_tables);
        let (offset, len) = tables_list.get_entry(row);
        for i in offset..offset + len {
            let alias = DuckString::new(&mut { alias_data[i] }).as_str().to_string();
            let tbl = DuckString::new(&mut { table_data[i] }).as_str().to_string();
            tables.push(TableRef { alias, table: tbl });
        }
    }
    ```

    **Column 2 — relationships LIST(STRUCT(from_table, to_table, join_columns LIST(STRUCT(from, to)))):**
    Follow RESEARCH.md Pattern 3 for nested LIST-in-STRUCT. For each relationship row:
    - Read from_table (field 0) and to_table (field 1) as flat strings
    - Read join_columns (field 2) as a nested list vector
    - For each join_column entry, read from (field 0) and to (field 1)
    - Construct `Join { table: to_table, on: String::new(), from_cols: vec![], join_columns: jc_vec }`

    Note: The `table` field of Join is the JOIN target. For relationships, `to_table` is the joined table (the one we JOIN to from `from_table`). Use `to_table` as `Join.table`.

    **Column 3 — dimensions LIST(STRUCT(name, expr, source_table)):**
    Read three VARCHAR fields per struct. Construct `Dimension { name, expr, source_table: Some(st) if !st.is_empty() else None, dim_type: None, granularity: None }`.

    **Column 4 — time_dimensions LIST(STRUCT(name, expr, granularity)):**
    Read three VARCHAR fields. Construct `Dimension { name, expr, source_table: None, dim_type: Some("time".to_string()), granularity: Some(granularity) }`. Validate granularity is one of ["day", "week", "month", "year"] — return Err if not.

    **Column 5 — metrics LIST(STRUCT(name, expr, source_table)):**
    Same pattern as dimensions. Construct `Metric { name, expr, source_table: Some(st) if !st.is_empty() else None }`.

    **Assembling SemanticViewDefinition:**
    - `base_table`: first table's `table` field if tables non-empty, else String::new()
    - `dimensions`: concatenate dimensions (col 3) + time_dimensions (col 4)
    - `metrics`: from col 5
    - `filters`: vec![]
    - `joins`: from col 2
    - `facts`: vec![]
    - `tables`: from col 1

    4. Add `#[cfg(test)]` block with unit tests for pure helper logic (string reading from duckdb_string_t slice, granularity validation function, etc.).

    5. Update src/ddl/mod.rs to add `pub mod parse_args;`
  </implementation>
</feature>

<verification>
`cargo test` passes (unit tests in parse_args.rs and all existing tests). The parse_args module is accessible from ddl::parse_args.
</verification>

<success_criteria>
- src/ddl/parse_args.rs exists with parse_define_args function
- ParsedDefineArgs struct exported
- mod.rs has `pub mod parse_args`
- `cargo test` passes including parse_args unit tests
- time_dimensions inputs produce Dimension with dim_type: Some("time") and granularity validated
- Empty list columns produce empty Vecs without panic
</success_criteria>

<output>
After completion, create `.planning/phases/11.1-review-possible-ddl-and-query-syntax-options-and-bring-it-as-close-as-we-can-to-snowflake-semantic-views/11.1-02-SUMMARY.md`
</output>
