# Phase 11.1: Review DDL and Query Syntax — Research

**Researched:** 2026-03-02
**Domain:** Rust VScalar API / DuckDB STRUCT+LIST types / Snowflake Semantic View spec
**Confidence:** HIGH

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

**Deliverable**
- Write a short `DESIGN.md` in the phase directory with before/after syntax comparison and rationale
- Implement the new DDL and query interfaces in Rust
- Update all tests to use the new syntax
- Reference: Snowflake only (no dbt SL / Cube comparison needed)

**Query function rename**
- Rename `semantic_query` → `semantic_view` (no backward-compat alias — clean rename)
- Keep named array parameters: `dimensions := ['region', 'category']` — DuckDB idiomatic
- Keep `WHERE` clause as the filter mechanism — no inline filter parameter
- Query syntax supports table-qualified dimension/metric names: `dimensions := ['o.region', 'c.tier']`

**DDL: replace JSON blob with typed DuckDB STRUCT/LIST parameters**
- Replace `define_semantic_view('name', '{"base_table":...}')` (raw JSON string) with named parameters using DuckDB STRUCT and LIST types

New DDL signature:
```sql
SELECT define_semantic_view(
    'sales_view',
    tables           := [{'alias': 'o', 'table': 'orders'}, {'alias': 'c', 'table': 'customers'}],
    relationships    := [{'from_table': 'o', 'to_table': 'c', 'join_columns': [{'from': 'customer_id', 'to': 'id'}]}],
    dimensions       := [{'name': 'region', 'expr': 'o.region', 'source_table': 'o'}],
    time_dimensions  := [{'name': 'order_date', 'expr': 'o.created_at', 'granularity': 'day'}],
    metrics          := [{'name': 'revenue', 'expr': 'sum(o.amount)', 'source_table': 'o'}]
);
```

**DDL struct types (strongly typed, separate lists)**
- `tables`: `LIST(STRUCT(alias VARCHAR, table VARCHAR))`
- `relationships`: `LIST(STRUCT(from_table VARCHAR, to_table VARCHAR, join_columns LIST(STRUCT(from VARCHAR, to VARCHAR))))`
- `dimensions`: `LIST(STRUCT(name VARCHAR, expr VARCHAR, source_table VARCHAR))`
- `time_dimensions`: `LIST(STRUCT(name VARCHAR, expr VARCHAR, granularity VARCHAR))`
- `metrics`: `LIST(STRUCT(name VARCHAR, expr VARCHAR, source_table VARCHAR))`

**Table scoping model**
- Flat top-level lists (not nested per-table), with `source_table` field for disambiguation
- Dimension/metric names must be unique across all tables in a view (documented, not prevented)
- Nested per-table scoping deferred to v0.3.0

**Relationships: column-pair structs**
- `join_columns` is `LIST(STRUCT(from VARCHAR, to VARCHAR))` — explicit pair per column

### Claude's Discretion
- Internal `SemanticViewDefinition` model migration (can keep or restructure JSON storage)
- Rust VScalar signature details for nested STRUCT reading
- Whether to keep backward compat for existing stored JSON definitions or require re-definition
- Error message wording

### Deferred Ideas (OUT OF SCOPE)
- Nested per-table scoping (`tables := [{alias:'o', dimensions:[...], metrics:[...]}]`) — v0.3.0
- Backward-compat alias `semantic_query` — explicitly not added; clean rename only
- Inline filter parameter in query function — WHERE clause is sufficient
- Composite FK test coverage may be minimal in this phase
</user_constraints>

---

## Summary

Phase 11.1 is a **pure interface reshaping** phase — no new semantic capabilities are added. The work consists of: (1) redesigning the DDL function signature from a raw JSON string to named STRUCT/LIST parameters, (2) renaming `semantic_query` to `semantic_view`, (3) migrating the internal data model and expand logic, and (4) updating all test files.

The duckdb-rs 1.4.4 crate fully supports STRUCT and LIST `LogicalTypeHandle` construction in `VScalar::signatures()` via `LogicalTypeHandle::struct_type()` and `LogicalTypeHandle::list()`. Reading nested STRUCTs during `VScalar::invoke()` uses `DataChunkHandle::struct_vector(col_idx)` to get a `StructVector`, then `StructVector::child(field_idx, capacity)` to get each field as a `FlatVector` from which `duckdb_string_t` values are read. For the `relationships` parameter (LIST of STRUCT containing a nested LIST of STRUCT), reading uses `ListVector::struct_child()` → `StructVector::list_vector_child()` → inner child traversal. The API exists but nested LIST-of-STRUCT reading is underdocumented and requires careful understanding of `duckdb_list_entry` offsets.

The Snowflake semantic view YAML spec confirms the chosen terminology: `time_dimensions` (separate list), `expr`, `source_table`, and `relationship_columns` (we use `join_columns` with `from`/`to` as shorter field names). The internal JSON storage format can remain unchanged — the new STRUCT inputs are mapped to the existing `SemanticViewDefinition` struct before serialization, so old stored JSON is automatically backward-compatible (forward-compatible re-definition is not needed).

**Primary recommendation:** Implement DDL argument parsing via a new helper module `src/ddl/parse_args.rs` that translates STRUCT/LIST `DataChunkHandle` inputs into the existing `SemanticViewDefinition` model. This isolates the complex unsafe vector traversal code and keeps `define.rs` clean.

---

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| duckdb-rs | =1.4.4 | VScalar trait, LogicalTypeHandle, DataChunkHandle, StructVector | Already pinned in Cargo.toml |
| libduckdb-sys | =1.4.4 | Raw FFI for duckdb_list_entry, duckdb_string_t | Already pinned |
| serde_json | 1 | JSON serialization of internal model for catalog storage | Already in use |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| libduckdb-sys ffi | =1.4.4 | `duckdb_list_entry` struct for list offset/length reading | Reading LIST vectors from VScalar input |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| STRUCT/LIST typed params | Raw JSON string (current) | JSON is error-prone, no DuckDB type checking, poor UX |
| Internal JSON storage unchanged | Restructure JSON format | Restructuring breaks all existing stored views with no benefit |
| Named params via `ScalarFunctionSignature::exact` overloads | Multiple overload functions | Named params can't be expressed in VScalar `exact` — DuckDB named params only work on table functions; DDL uses positional args |

**Critical discovery:** DuckDB VScalar (`register_scalar_function_with_state`) does NOT support named parameters — only positional parameters via `ScalarFunctionSignature::exact`. The new DDL interface uses positional parameters in a fixed order: `(name, tables, relationships, dimensions, time_dimensions, metrics)`. The SQL call-site uses `param := value` notation as DuckDB syntax sugar that maps to positional args when the function is registered with named param declarations — **but VScalar does not support this**. See Architecture Patterns for the resolution.

---

## Architecture Patterns

### Recommended Project Structure

```
src/
├── ddl/
│   ├── define.rs         # DefineSemanticView VScalar — signature + invoke (MODIFIED)
│   ├── parse_args.rs     # NEW: STRUCT/LIST → SemanticViewDefinition translation
│   ├── drop.rs           # DropSemanticView — only function name change in lib.rs
│   ├── list.rs           # ListSemanticViewsVTab — no changes needed
│   ├── describe.rs       # DescribeSemanticViewVTab — minor output field updates
│   └── mod.rs            # re-exports
├── query/
│   ├── table_function.rs # SemanticViewVTab — rename semantic_query → semantic_view
│   │                     # add table-qualified dimension name parsing
│   ├── explain.rs        # ExplainSemanticViewVTab — rename in lib.rs only
│   ├── error.rs          # no changes
│   └── mod.rs
├── expand.rs             # resolve table-qualified names ('o.region' → dim lookup)
├── model.rs              # SemanticViewDefinition — add `tables` field, update Join
├── catalog.rs            # no changes
└── lib.rs                # rename registration strings
```

### Pattern 1: Named Parameters for DDL — Resolution

**What:** DuckDB VScalar does not support named parameters. The CONTEXT.md target SQL uses `param := value` syntax which IS supported by DuckDB table functions (VTab), not scalar functions.

**Resolution options (Claude's Discretion):**
1. **Register `define_semantic_view` as a VTab** instead of VScalar — allows named params. However, VTab `bind()` can execute SQL (unlike VScalar `invoke()` which holds the execution lock), so this may be feasible.
2. **Use positional args** with a fixed ordering — cleaner for VScalar, but user must know the order.
3. **Keep VScalar but accept named-param-like syntax** — DuckDB allows `func(pos1, named1 := val1, ...)` only for table functions.

**Recommended resolution:** The existing `define_semantic_view` is registered as a VScalar (scalar function). The new multi-arg signature with 5 named list args is better suited to a **table function** pattern. However, since VScalar can only be called with `SELECT func(...)` which is already the DDL pattern, the simplest path is **positional VScalar with fixed order**:

```sql
SELECT define_semantic_view(
    'sales_view',                                    -- arg 0: name VARCHAR
    [{'alias':'o','table':'orders'}],                -- arg 1: tables LIST(STRUCT)
    [{'from_table':'o','to_table':'c',...}],         -- arg 2: relationships LIST(STRUCT)
    [{'name':'region','expr':'o.region',...}],       -- arg 3: dimensions LIST(STRUCT)
    [{'name':'order_date','expr':'o.created_at',...}], -- arg 4: time_dimensions LIST(STRUCT)
    [{'name':'revenue','expr':'sum(o.amount)',...}]  -- arg 5: metrics LIST(STRUCT)
);
```

DuckDB supports this with STRUCT literal syntax `{'key': value}` and LIST literal syntax `[...]`. This is clean and idiomatic. The `param := value` syntax in CONTEXT.md was aspirational — plain positional args in order is the correct implementation.

**Alternatively**, the function can be overloaded to handle missing optional parameters via multiple `ScalarFunctionSignature::exact` entries (e.g., omitting `relationships` and/or `time_dimensions`). This is cleaner UX.

### Pattern 2: Reading LIST(STRUCT) in VScalar invoke

**Source:** Verified from duckdb-rs 1.4.4 source (`src/core/vector.rs`)

Reading a `LIST(STRUCT(alias VARCHAR, table VARCHAR))` parameter at column index `col_idx`:

```rust
// Step 1: Get the list vector for this column
let list_vec = input.list_vector(col_idx);

// Step 2: Get list entries (offset/length pairs per row)
let entries = list_vec.entries.as_slice::<duckdb_list_entry>(input.len());

// Step 3: Get the child as a StructVector
let total_child_len = list_vec.len(); // total child elements
let struct_child = list_vec.struct_child(total_child_len);

// Step 4: For each struct field, get as FlatVector
let alias_vec = struct_child.child(0, total_child_len); // field 0: alias
let table_vec = struct_child.child(1, total_child_len); // field 1: table

// Step 5: For each row, iterate its list elements using offset/length
for row in 0..input.len() {
    let entry = entries[row];
    let offset = entry.offset as usize;
    let len = entry.length as usize;
    for elem_idx in offset..offset + len {
        let alias = read_str_from_flat_vec(&alias_vec, elem_idx);
        let table = read_str_from_flat_vec(&table_vec, elem_idx);
    }
}
```

**Caveat:** The `entries` field in `ListVector` is private. Access via `list_vec.get_entry(idx)` which returns `(offset, length)` as `(usize, usize)`. Confirmed from vector.rs line 232-235.

### Pattern 3: Reading Nested LIST(STRUCT(from, to)) inside STRUCT

For `relationships` which contains `join_columns LIST(STRUCT(from VARCHAR, to VARCHAR))`:

```rust
// relationships column: LIST(STRUCT(from_table, to_table, join_columns LIST(STRUCT(from, to))))
let rel_list_vec = input.list_vector(rel_col_idx);
let rel_struct_child = rel_list_vec.struct_child(total_rels);

// Fields 0,1: from_table, to_table as FlatVectors
let from_table_vec = rel_struct_child.child(0, total_rels);
let to_table_vec = rel_struct_child.child(1, total_rels);
// Field 2: join_columns is itself a LIST — get as ListVector
let jc_list_vec = rel_struct_child.list_vector_child(2);
let jc_struct_child = jc_list_vec.struct_child(total_jc);
let from_col_vec = jc_struct_child.child(0, total_jc);
let to_col_vec = jc_struct_child.child(1, total_jc);
```

**Warning (LOW confidence):** The `ListVector::entries` field access for offset/length in the nested list uses `get_entry(idx)`. The child element count (`total_jc`) must be determined from `jc_list_vec.len()`. This pattern is not directly shown in duckdb-rs examples but follows from the API structure.

### Pattern 4: Internal Model Migration Strategy

The `SemanticViewDefinition` in `src/model.rs` must be updated:

**Current `Join` struct** (keep backward-compat with stored JSON):
```rust
pub struct Join {
    pub table: String,
    pub on: String,          // old format — raw ON clause string
    pub from_cols: Vec<String>,  // Phase 11 format
}
```

**New model additions needed:**

```rust
// New: table alias entry (for the `tables` parameter)
pub struct TableRef {
    pub alias: String,
    pub table: String,
}

// New: column-pair relationship (replaces from_cols / on)
pub struct JoinColumn {
    pub from: String,
    pub to: String,
}

// Updated Join — must still deserialize old JSON with `on` field
pub struct Join {
    pub table: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub on: String,           // legacy: kept for backward compat with stored JSON
    #[serde(default)]
    pub from_cols: Vec<String>,  // Phase 11 format: deprecated in 11.1
    #[serde(default)]
    pub join_columns: Vec<JoinColumn>,  // Phase 11.1: new format
}
```

**Storage decision (Claude's Discretion):** The JSON stored in `semantic_layer._definitions` can be updated to include `tables` (as a new optional field) and `join_columns`. Old stored definitions without `tables` still load fine (serde defaults to empty). Old definitions with `on` or `from_cols` in `join` still load fine. **Recommendation: keep the internal storage format mostly as-is, add `tables: Vec<TableRef>` with `#[serde(default)]`, and add `join_columns: Vec<JoinColumn>` alongside the existing `on`/`from_cols` fields with serde defaults.**

### Pattern 5: Table-Qualified Dimension Names in semantic_view

The query function rename from `semantic_query` to `semantic_view` also adds support for `dimensions := ['o.region', 'c.tier']`. These are **not** raw SQL expressions — they are dimension lookup keys that include an optional table alias prefix.

Resolution in `expand.rs`:
1. When a dimension name contains `.`, split on first `.` to get `(alias, bare_name)`
2. Look up dimension by `bare_name` (case-insensitive) AND verify `source_table == alias`
3. If no match with alias, fall back to bare name lookup (backward compat)
4. Error message should include the full `o.region` string as the "unknown" name

This is pure Rust logic in `expand.rs` — no API changes required.

### Anti-Patterns to Avoid

- **Calling DuckDB SQL from VScalar `invoke()`:** The execution lock is held during `invoke`. All STRUCT parsing must be pure Rust memory reads with no SQL calls (persist to DuckDB table still uses the separate `persist_conn` pattern).
- **Building JSON by hand from STRUCT inputs:** Always parse STRUCTs → Rust structs → serialize to JSON via `serde_json`. Never concatenate JSON strings manually.
- **Multiple ScalarFunctionSignature overloads for required params:** The 5 list params (tables, relationships, dimensions, time_dimensions, metrics) should all be required. Use empty list `[]` for unused params rather than optional overloads, to keep the signature simple.
- **Assuming `StructVector::entries` is public:** Use `get_entry(idx)` for `ListVector` offset/length access.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| STRUCT type declaration | Custom FFI struct_type calls | `LogicalTypeHandle::struct_type(&[("field", LogicalTypeHandle::from(...))])` | Already in duckdb-rs 1.4.4 |
| LIST type declaration | Custom FFI list_type calls | `LogicalTypeHandle::list(&child_type)` | Already in duckdb-rs 1.4.4 |
| String reading from vectors | Custom duckdb_string_t layout | Reuse existing `duckdb::types::DuckString::new(&mut raw).as_str()` pattern | Already established in project code |
| JSON serialization | Custom serialize | `serde_json::to_string(&def)` | Already in use |
| Argument name → index mapping | Hash map | Fixed positional order in `ScalarFunctionSignature::exact` | VScalar doesn't support named params |

---

## Common Pitfalls

### Pitfall 1: VScalar Does Not Support Named Parameters
**What goes wrong:** Attempting to register `define_semantic_view` with `param := value` call syntax fails — DuckDB named parameters only work on table functions (VTab), not scalar functions.
**Why it happens:** The `register_scalar_function_with_state` API uses `ScalarFunctionSignature::exact` which is purely positional.
**How to avoid:** Use fixed positional order. Document the parameter order in the function's return error message and in DESIGN.md.
**Warning signs:** If you try to call `func(name := 'x', tables := [...])` on a scalar function, DuckDB will error with a binding error.

### Pitfall 2: ListVector `entries` Field Is Private
**What goes wrong:** Trying to access `list_vec.entries.as_slice::<duckdb_list_entry>()` directly won't compile — `entries` is private in `ListVector`.
**Why it happens:** The Rust API exposes `get_entry(idx) -> (usize, usize)` instead.
**How to avoid:** Always use `list_vec.get_entry(idx)` to get `(offset, length)` for each row. Use `list_vec.len()` for total child count.

### Pitfall 3: STRUCT Field Index Ordering Matters
**What goes wrong:** `StructVector::child(idx, capacity)` returns field at position `idx` as declared in the STRUCT type. If the order in `LogicalTypeHandle::struct_type` differs from what the user passes, fields are misread.
**Why it happens:** DuckDB maps struct fields by position, not by name, in vector reads.
**How to avoid:** Declare struct field order in `signatures()` and read fields in the exact same order in `invoke()`. Add assertions or use `struct_vec.child_name(idx)` to verify field name at the right index during debug builds.

### Pitfall 4: `table` Is a Reserved Keyword in Field Names
**What goes wrong:** The struct field `{'alias': 'o', 'table': 'orders'}` uses `table` which is a SQL reserved word. DuckDB STRUCT literal syntax uses `{key: value}` where keys are identifiers — reserved words as STRUCT keys may need quoting.
**Why it happens:** DuckDB STRUCT key identifiers follow SQL identifier rules.
**How to avoid:** Test the literal `{'alias': 'o', 'table': 'orders'}` in DuckDB to confirm it parses without quoting. If not, rename to `tbl` or `source`. CONTEXT.md uses `table` so this needs verification before committing to the field name.
**Warning signs:** SQL parse error on the struct literal during integration test.

### Pitfall 5: Nested LIST in STRUCT — Child Count Must Be Queried Separately
**What goes wrong:** For `relationships LIST(STRUCT(from_table, to_table, join_columns LIST(STRUCT(from, to))))`, the inner list's child count cannot be inferred from the outer list's row count.
**Why it happens:** Each `join_columns` list has its own length stored separately in the nested list vector.
**How to avoid:** Always call `inner_list_vec.len()` to get the total child element count before accessing child vectors. Initialize with that capacity.

### Pitfall 6: `time_dimensions` Must Not Have `dim_type` Field
**What goes wrong:** The current `Dimension` model has a `dim_type` field (`"time"`) that the old JSON-based DDL required. The new STRUCT interface uses a separate `time_dimensions` list — the `dim_type` field is no longer written.
**Why it happens:** Old model design mixed time-detection into the `Dimension` struct.
**How to avoid:** When parsing `time_dimensions` input, construct `Dimension` with `dim_type: Some("time".to_string())` and `granularity: Some(...)` so the existing expand logic (`dim_type.as_deref() == Some("time")`) continues to work. The internal model doesn't need to change.

### Pitfall 7: Empty Optional Lists vs. NULL
**What goes wrong:** If the caller omits `relationships := []` entirely, DuckDB may pass NULL instead of an empty list to the function.
**Why it happens:** VScalar receives NULL for omitted arguments in some DuckDB versions.
**How to avoid:** In `invoke()`, check for NULL validity on each argument before reading it. Return empty Vec for NULL inputs. Use `flat_vector.row_is_null(0)` or the validity mask on the list vector's internal `entries` ptr to detect NULL.

---

## Code Examples

Verified patterns from duckdb-rs 1.4.4 source:

### Declaring LIST(STRUCT) in signatures()
```rust
// Source: duckdb-rs 1.4.4 src/core/logical_type.rs (verified)
// LIST(STRUCT(alias VARCHAR, table VARCHAR))
let varchar = LogicalTypeHandle::from(LogicalTypeId::Varchar);
let table_struct = LogicalTypeHandle::struct_type(&[
    ("alias", LogicalTypeHandle::from(LogicalTypeId::Varchar)),
    ("table", LogicalTypeHandle::from(LogicalTypeId::Varchar)),
]);
let tables_type = LogicalTypeHandle::list(&table_struct);
```

### Declaring nested LIST(STRUCT(from, to)) inside a STRUCT
```rust
// join_columns LIST(STRUCT(from VARCHAR, to VARCHAR))
let col_pair_struct = LogicalTypeHandle::struct_type(&[
    ("from", LogicalTypeHandle::from(LogicalTypeId::Varchar)),
    ("to", LogicalTypeHandle::from(LogicalTypeId::Varchar)),
]);
let join_columns_type = LogicalTypeHandle::list(&col_pair_struct);

// relationships LIST(STRUCT(from_table, to_table, join_columns LIST(STRUCT(from, to))))
let rel_struct = LogicalTypeHandle::struct_type(&[
    ("from_table", LogicalTypeHandle::from(LogicalTypeId::Varchar)),
    ("to_table", LogicalTypeHandle::from(LogicalTypeId::Varchar)),
    ("join_columns", join_columns_type),
]);
let relationships_type = LogicalTypeHandle::list(&rel_struct);
```

### Reading LIST(STRUCT) in invoke()
```rust
// Source: duckdb-rs 1.4.4 src/core/vector.rs (verified)
// Read tables LIST(STRUCT(alias VARCHAR, table VARCHAR)) from column 1
let tables_list = input.list_vector(1);
let total_table_elems = tables_list.len();
let tables_struct = tables_list.struct_child(total_table_elems);

let alias_fv = tables_struct.child(0, total_table_elems);
let table_fv = tables_struct.child(1, total_table_elems);
let alias_data = alias_fv.as_slice_with_len::<duckdb_string_t>(total_table_elems);
let table_data = table_fv.as_slice_with_len::<duckdb_string_t>(total_table_elems);

let mut result = Vec::new();
for row in 0..input.len() {
    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 table = DuckString::new(&mut { table_data[i] }).as_str().to_string();
        result.push(TableRef { alias, table });
    }
}
```

### Registering the new multi-arg VScalar
```rust
// signatures() — fixed positional order
fn signatures() -> Vec<ScalarFunctionSignature> {
    vec![ScalarFunctionSignature::exact(
        vec![
            LogicalTypeHandle::from(LogicalTypeId::Varchar),  // 0: name
            tables_list_type,                                  // 1: tables
            relationships_list_type,                           // 2: relationships
            dimensions_list_type,                              // 3: dimensions
            time_dimensions_list_type,                         // 4: time_dimensions
            metrics_list_type,                                 // 5: metrics
        ],
        LogicalTypeHandle::from(LogicalTypeId::Varchar),       // returns view name
    )]
}
```

### semantic_view table function rename (lib.rs)
```rust
// Before:
con.register_table_function_with_extra_info::<SemanticViewVTab, _>(
    "semantic_query",
    &query_state,
)?;

// After:
con.register_table_function_with_extra_info::<SemanticViewVTab, _>(
    "semantic_view",
    &query_state,
)?;
```

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `define_semantic_view('name', '{json}')` raw JSON | `define_semantic_view('name', [...], [...], ...)` STRUCT/LIST | Phase 11.1 | Type-safe, DuckDB validates structure |
| `semantic_query(...)` table function | `semantic_view(...)` table function | Phase 11.1 | Aligns with Snowflake naming |
| `Dimension.dim_type = "time"` + `Dimension.granularity` | Separate `time_dimensions` list input → same internal model | Phase 11.1 | Cleaner user API, same internal representation |
| `Join.on` raw SQL string | `Join.join_columns` struct pairs | Phase 11.1 | Structured FK declarations, supports composite keys |
| `Join.from_cols: Vec<String>` (Phase 11) | `Join.join_columns: Vec<JoinColumn>` (Phase 11.1) | Phase 11.1 | Column-pair structs replace parallel arrays |

**Deprecated/outdated:**
- `Join.from_cols: Vec<String>` — replaced by `join_columns: Vec<JoinColumn>` in Phase 11.1. Keep for serde backward compat with stored JSON, but do not write new definitions using this field.
- `Join.on: String` — raw SQL ON clause, kept for backward compat with pre-Phase-11 stored definitions.

---

## Open Questions

1. **Is `table` a reserved word in DuckDB STRUCT literal keys?**
   - What we know: SQL reserved words can cause parse issues in identifier positions
   - What's unclear: Whether DuckDB STRUCT literal keys are treated as identifiers or strings
   - Recommendation: Test `SELECT {'table': 'orders'}` in DuckDB early in Plan 1. If it fails, rename the field to `tbl` in both the spec and implementation.

2. **VScalar named-parameter support — did DuckDB 1.4.4 add this?**
   - What we know: Named params are well-documented for VTab (table functions); VScalar docs show only positional params
   - What's unclear: Whether there is an undocumented path to register named params on a scalar function in 1.4.4
   - Recommendation: Assume positional for Phase 11.1 planning. If named params become available for VScalar in a future version, the API can be extended.

3. **Are empty list params `[]` for omitted relationships/time_dimensions better handled as NULL or as empty list?**
   - What we know: DuckDB passes `[]` (empty list) when the user writes `[]`, but may pass NULL for entirely missing arguments if the function has fewer required params
   - What's unclear: Whether a VScalar with 6 positional args can accept fewer args (likely not without overloads)
   - Recommendation: Require all 6 args in the exact signature. User writes `[]` for empty lists. Add a convenience overload for the most common case (no relationships, no time_dimensions): `(name, tables, dimensions, metrics)`.

4. **Internal model: add `tables: Vec<TableRef>` field or keep only `base_table: String`?**
   - What we know: The new DDL takes a `tables` list with aliases; the `base_table` field was used for single-table views
   - What's unclear: Whether multi-table views should deprecate `base_table` or keep it as the primary source
   - Recommendation: Keep `base_table` as the first table's `table` field (for backward compat with existing `expand()` SQL generation). Add `tables: Vec<TableRef>` with `#[serde(default)]` but use it only for alias registration. The expand logic CTE builder can use `base_table` unchanged.

---

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | sqllogictest (via `duckdb_sqllogictest` Python package) + `cargo test` |
| Config file | None (sqllogictest is invoked via Makefile target) |
| Quick run command | `cargo test` (unit tests only, no extension build needed) |
| Full suite command | `make test` (builds extension, runs all .test files) |

### Phase Requirements → Test Map

Phase 11.1 has no formal REQ-XX IDs but the CONTEXT.md deliverables imply:

| Behavior | Test Type | Command | File |
|----------|-----------|---------|------|
| `define_semantic_view` accepts new STRUCT/LIST args | Integration | `make test` | `test/sql/phase2_ddl.test` — UPDATE |
| `define_or_replace_semantic_view` updated | Integration | `make test` | `test/sql/phase2_ddl.test` — UPDATE |
| `drop_semantic_view` unchanged signature | Integration | `make test` | `test/sql/phase2_ddl.test` — VERIFY |
| `semantic_view` (renamed) returns correct results | Integration | `make test` | `test/sql/phase4_query.test` — UPDATE |
| Table-qualified dims `['o.region']` resolve correctly | Integration | `make test` | `test/sql/phase4_query.test` — ADD |
| Internal model maps STRUCT inputs to correct JSON | Unit | `cargo test` | `src/ddl/parse_args.rs` — NEW |
| Expand handles `time_dimensions` from new model | Unit | `cargo test` | `src/expand.rs` tests — VERIFY |
| DESIGN.md written | Manual | Review | `.planning/phases/11.1-*/DESIGN.md` — NEW |

### Wave 0 Gaps
- [ ] `src/ddl/parse_args.rs` — new module for STRUCT/LIST → model parsing; tests in `mod tests` block
- [ ] Update `test/sql/phase2_ddl.test` to new STRUCT/LIST syntax
- [ ] Update `test/sql/phase4_query.test` to use `semantic_view` instead of `semantic_query`

*(Existing test infrastructure covers all requirements — no new test framework needed)*

---

## Sources

### Primary (HIGH confidence)
- duckdb-rs 1.4.4 source (`~/.cargo/registry/src/.../duckdb-1.4.4/src/core/vector.rs`) — StructVector, ListVector APIs confirmed
- duckdb-rs 1.4.4 source (`~/.cargo/registry/src/.../duckdb-1.4.4/src/vscalar/mod.rs`) — VScalar trait, ScalarFunctionSignature::exact confirmed positional-only
- Context7 `/websites/rs_duckdb_duckdb` — `LogicalTypeHandle::struct_type`, `LogicalTypeHandle::list`, `DataChunkHandle::struct_vector` confirmed
- docs.rs `duckdb 1.4.4` — `LogicalTypeHandle` methods verified
- Snowflake YAML spec (https://docs.snowflake.com/en/user-guide/views-semantic/semantic-view-yaml-spec) — terminology: `time_dimensions`, `expr`, `relationship_columns`, `left_column`/`right_column`

### Secondary (MEDIUM confidence)
- Project CONTEXT.md — user decisions locked and verbatim captured above
- Existing project source (`src/ddl/define.rs`, `src/expand.rs`, `src/model.rs`) — confirmed current architecture patterns

### Tertiary (LOW confidence)
- Nested LIST-in-STRUCT VScalar reading pattern — inferred from API structure; not shown in official examples. Needs integration test to validate.
- `table` keyword safety in STRUCT literal keys — needs empirical test in DuckDB 1.4.4.

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — duckdb-rs 1.4.4 source confirmed all needed APIs exist
- Architecture: HIGH for top-level patterns; MEDIUM for nested LIST-in-STRUCT reading; LOW for `table` keyword safety
- Pitfalls: HIGH — derived from source inspection; VScalar named-param limitation verified from source
- Snowflake alignment: HIGH — verified from official Snowflake YAML spec

**Research date:** 2026-03-02
**Valid until:** 2026-04-02 (duckdb-rs pinned at =1.4.4, no API drift risk within 30 days)
