---
phase: 24-pk-fk-model
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/model.rs
autonomous: true
requirements:
  - MDL-01
  - MDL-02
  - MDL-03
  - MDL-04
  - MDL-05

must_haves:
  truths:
    - "TableRef stores primary key columns alongside alias and physical table"
    - "Join stores from_alias, fk_columns, and optional name alongside existing fields"
    - "Old stored JSON without new fields still deserializes correctly (backward compat)"
    - "Composite primary keys (multi-column) round-trip through JSON serialization"
    - "Qualified alias.name syntax can be split to extract source table alias"
  artifacts:
    - path: "src/model.rs"
      provides: "Extended TableRef (pk_columns) and Join (from_alias, fk_columns, name) structs"
      contains: "pk_columns"
    - path: "src/model.rs"
      provides: "Phase 24 serde round-trip and backward compat tests"
      contains: "phase24_model_tests"
  key_links:
    - from: "src/model.rs::TableRef"
      to: "serde_json"
      via: "#[serde(default)] on pk_columns"
      pattern: "serde\\(default\\)"
    - from: "src/model.rs::Join"
      to: "serde_json"
      via: "#[serde(default)] on from_alias, fk_columns, name"
      pattern: "from_alias"
---

<objective>
Extend the model structs in `model.rs` to support Snowflake-style PK/FK relationship metadata.

Purpose: The semantic view model must be capable of representing primary keys per table, FK-based relationships with source aliases and optional names, and source table aliases on dimensions/metrics from qualified `alias.name` syntax. This is the data model foundation for Phases 25-28.

Output: Updated `TableRef` and `Join` structs with new fields, comprehensive serde round-trip tests, backward compatibility 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/24-pk-fk-model/24-RESEARCH.md
@src/model.rs

<interfaces>
<!-- Current structs being extended. Executor should read these directly from model.rs. -->

From src/model.rs (current):
```rust
pub struct TableRef {
    pub alias: String,
    pub table: String,
}

pub struct Join {
    pub table: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub on: String,
    #[serde(default)]
    pub from_cols: Vec<String>,
    #[serde(default)]
    pub join_columns: Vec<JoinColumn>,
}
```

Target state (from RESEARCH.md Pattern 1 and Pattern 2):
```rust
pub struct TableRef {
    pub alias: String,
    pub table: String,
    #[serde(default)]
    pub pk_columns: Vec<String>,
}

pub struct Join {
    pub table: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub on: String,
    #[serde(default)]
    pub from_cols: Vec<String>,
    #[serde(default)]
    pub join_columns: Vec<JoinColumn>,
    #[serde(default)]
    pub from_alias: String,
    #[serde(default)]
    pub fk_columns: Vec<String>,
    #[serde(default)]
    pub name: Option<String>,
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Extend TableRef and Join structs with PK/FK fields and add serde tests</name>
  <files>src/model.rs</files>
  <behavior>
    - Test: TableRef with pk_columns round-trips through JSON (MDL-01)
    - Test: TableRef with composite PK (2+ columns) round-trips preserving order (MDL-04)
    - Test: Old TableRef JSON without pk_columns deserializes with empty vec (backward compat)
    - Test: Join with from_alias, fk_columns, name round-trips through JSON (MDL-02, MDL-05)
    - Test: Old Join JSON with only `table` and `on` fields still deserializes (backward compat for from_alias="", fk_columns=[], name=None)
    - Test: Relationship name None vs Some("name") serialization (MDL-05)
    - Test: Qualified name "orders.revenue" splits to alias="orders", bare_name="revenue" (MDL-03 utility)
    - Test: Unqualified name "revenue" has no dot split (MDL-03 edge case)
    - Test: Complete PK/FK SemanticViewDefinition round-trips (TPC-H-style 3-table example from RESEARCH.md Example 5)
  </behavior>
  <action>
1. Add `pk_columns: Vec<String>` to `TableRef` with `#[serde(default)]`. Place it after the `table` field. Do NOT use `Option<Vec<String>>` -- the empty-vec pattern with serde default is the established convention in this codebase.

2. Add three new fields to `Join` struct, placed AFTER the existing `join_columns` field:
   - `from_alias: String` with `#[serde(default)]` -- source table alias for PK/FK relationships. Empty string = legacy backward compat.
   - `fk_columns: Vec<String>` with `#[serde(default)]` -- FK column names on the source side. These reference the target table's PK (inferred from TableRef.pk_columns at expansion time in Phase 26).
   - `name: Option<String>` with `#[serde(default)]` -- optional relationship name from `name AS ...` syntax.

3. Update the existing `phase11_1_model_tests::table_ref_roundtrip` test to include `pk_columns: vec![]` in the TableRef constructor (the struct literal will now require it since it's a non-Option field without Default on the struct -- but TableRef derives Default, so existing code using `Default::default()` is fine; only struct literals need updating).

4. Update `phase11_1_model_tests::semantic_view_definition_with_tables_roundtrip` to include `pk_columns: vec![]` in the TableRef constructor.

5. Add a new `mod phase24_model_tests` test module (inside the existing `mod tests`) with these tests:
   - `table_ref_with_pk_columns_roundtrip`: Single PK column case
   - `table_ref_composite_pk_roundtrip`: Multi-column PK (e.g., `["l_orderkey", "l_linenumber"]`), verify order preserved
   - `old_table_ref_json_without_pk_columns_deserializes`: Parse `{"alias":"o","table":"orders"}` and assert pk_columns is empty vec
   - `join_with_pkfk_fields_roundtrip`: Join with from_alias, fk_columns, name set
   - `old_join_json_without_pkfk_fields_deserializes`: Parse old JSON `{"table":"customers","on":"a.id=b.id"}` and assert from_alias="", fk_columns=[], name=None
   - `join_relationship_name_none_vs_some`: Test both None and Some("rel_name") serialization
   - `qualified_name_split`: Helper test for `alias.name` -> (alias, bare_name) parsing (just test the string logic, no struct needed)
   - `unqualified_name_no_split`: Name without dot returns as-is
   - `complete_pkfk_definition_roundtrip`: Full SemanticViewDefinition with 3 tables, 2 joins, dimensions with source_table, metrics -- based on RESEARCH.md Example 5

6. IMPORTANT: All new field types (`Vec<String>`, `String`, `Option<String>`) already implement `arbitrary::Arbitrary`, so the `#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]` derive on both structs will continue to compile.

7. IMPORTANT: Do NOT add `deny_unknown_fields` to any struct. Do NOT change any existing field semantics. Do NOT repurpose `join_columns` for FK storage.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test model::tests -x</automated>
  </verify>
  <done>
    - TableRef has `pk_columns: Vec<String>` with `#[serde(default)]`
    - Join has `from_alias: String`, `fk_columns: Vec<String>`, `name: Option<String>` all with `#[serde(default)]`
    - All 9+ new tests in `phase24_model_tests` pass
    - All existing tests in `phase11_model_tests`, `phase11_1_model_tests`, `phase12_model_tests` pass unchanged (except struct literal updates)
    - `cargo test model::tests` shows 0 failures
  </done>
</task>

</tasks>

<verification>
```bash
cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test model::tests -x
```
All model tests pass, including backward compat tests for old JSON without new fields.
</verification>

<success_criteria>
- `cargo test model::tests` passes with 0 failures
- TableRef.pk_columns stores single and composite primary keys
- Join.from_alias, Join.fk_columns, Join.name store relationship metadata
- Old JSON without new fields deserializes with serde defaults (backward compat proven by tests)
</success_criteria>

<output>
After completion, create `.planning/phases/24-pk-fk-model/24-01-SUMMARY.md`
</output>
