---
phase: 43-metadata-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/model.rs
autonomous: true
requirements:
  - META-01
  - META-06
  - META-07

must_haves:
  truths:
    - "AccessModifier enum exists with Public (default) and Private variants"
    - "SemanticViewDefinition has a comment: Option<String> field"
    - "TableRef, Dimension, Metric, Fact all have comment: Option<String> and synonyms: Vec<String> fields"
    - "Metric and Fact have access: AccessModifier field"
    - "All new fields use #[serde(default)] and appropriate skip_serializing_if"
    - "Pre-v0.6.0 JSON without new fields deserializes successfully with correct defaults"
    - "JSON with new fields roundtrips correctly"
  artifacts:
    - path: "src/model.rs"
      provides: "AccessModifier enum, metadata fields on all structs"
      contains: "pub enum AccessModifier"
  key_links:
    - from: "src/model.rs AccessModifier"
      to: "Metric.access and Fact.access fields"
      via: "struct field type"
      pattern: "pub access: AccessModifier"
---

<objective>
Add metadata model fields (COMMENT, SYNONYMS, PRIVATE/PUBLIC) to all semantic view structs with backward-compatible serde attributes.

Purpose: Establish the data model foundation that the parser (Plan 02) will populate and the expansion engine will enforce. Without these fields, no metadata can flow through the system.

Output: Updated model.rs with AccessModifier enum, metadata fields on all 5 structs, and comprehensive backward compatibility tests.
</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/43-metadata-foundation/43-RESEARCH.md

<interfaces>
<!-- Current model.rs struct signatures that will be modified -->
From src/model.rs:
```rust
pub struct TableRef {
    pub alias: String,
    pub table: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub pk_columns: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub unique_constraints: Vec<Vec<String>>,
}

pub struct Dimension {
    pub name: String,
    pub expr: String,
    #[serde(default)]
    pub source_table: Option<String>,
    #[serde(default)]
    pub output_type: Option<String>,
}

pub struct Metric {
    pub name: String,
    pub expr: String,
    #[serde(default)]
    pub source_table: Option<String>,
    #[serde(default)]
    pub output_type: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub using_relationships: Vec<String>,
}

pub struct Fact {
    pub name: String,
    pub expr: String,
    #[serde(default)]
    pub source_table: Option<String>,
    #[serde(default)]
    pub output_type: Option<String>,
}

pub struct SemanticViewDefinition {
    pub base_table: String,
    #[serde(default)]
    pub tables: Vec<TableRef>,
    pub dimensions: Vec<Dimension>,
    pub metrics: Vec<Metric>,
    #[serde(default)]
    pub joins: Vec<Join>,
    #[serde(default)]
    pub facts: Vec<Fact>,
    #[serde(default)]
    pub column_type_names: Vec<String>,
    #[serde(default)]
    pub column_types_inferred: Vec<u32>,
    #[serde(default)]
    pub created_on: Option<String>,
    #[serde(default)]
    pub database_name: Option<String>,
    #[serde(default)]
    pub schema_name: Option<String>,
}

// Existing pattern for enums with is_default:
pub enum Cardinality {
    #[default]
    ManyToOne,
    OneToOne,
}
impl Cardinality {
    pub fn is_default(&self) -> bool {
        matches!(self, Self::ManyToOne)
    }
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add AccessModifier enum and metadata fields to all model structs</name>
  <files>src/model.rs</files>
  <read_first>
    - src/model.rs (full file -- see current struct definitions, serde patterns, Cardinality enum pattern)
  </read_first>
  <behavior>
    - Test 1 (backward compat): JSON string with no comment/synonyms/access fields deserializes to SemanticViewDefinition with comment=None, all dims/metrics/facts/tables have comment=None, synonyms=[], access=Public
    - Test 2 (roundtrip with comment): SemanticViewDefinition with comment=Some("test") serializes to JSON containing "comment":"test" and deserializes back identically
    - Test 3 (roundtrip with synonyms): Dimension with synonyms=vec!["a","b"] serializes with "synonyms":["a","b"] and roundtrips
    - Test 4 (roundtrip with access): Metric with access=Private serializes with "access":"Private" and roundtrips
    - Test 5 (skip_serializing_if): Metric with access=Public (default) does NOT include "access" key in JSON output; Vec::is_empty synonyms omitted; None comment omitted
    - Test 6 (AccessModifier::is_default): Public.is_default() == true, Private.is_default() == false
    - Test 7 (pre-v0.6.0 full JSON): A JSON blob matching v0.5.5 format (with tables, dimensions, metrics, facts, joins, created_on, database_name, schema_name -- but NO comment/synonyms/access fields) deserializes with all defaults
  </behavior>
  <action>
Add the `AccessModifier` enum and metadata fields to src/model.rs:

1. **Add AccessModifier enum** (after the Cardinality enum, around line 115):
```rust
/// Access modifier for facts and metrics.
/// Default is Public -- private items cannot be queried directly
/// but can be referenced by derived metric expressions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum AccessModifier {
    #[default]
    Public,
    Private,
}

impl AccessModifier {
    /// Returns `true` when the variant is the default (`Public`).
    /// Used by `serde(skip_serializing_if)` to omit the field from JSON
    /// when it matches the default, preserving backward-compatible output.
    #[must_use]
    pub fn is_default(&self) -> bool {
        matches!(self, Self::Public)
    }
}
```

2. **Add fields to TableRef** (after unique_constraints):
```rust
    /// Optional human-readable comment for this table entry.
    /// Old stored JSON without this field deserializes to None.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,
    /// Informational synonyms (aliases) for this table entry.
    /// Old stored JSON without this field deserializes to empty Vec.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub synonyms: Vec<String>,
```

3. **Add fields to Dimension** (after output_type):
```rust
    /// Optional human-readable comment for this dimension.
    /// Old stored JSON without this field deserializes to None.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,
    /// Informational synonyms (aliases) for this dimension.
    /// Old stored JSON without this field deserializes to empty Vec.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub synonyms: Vec<String>,
```

4. **Add fields to Metric** (after using_relationships):
```rust
    /// Optional human-readable comment for this metric.
    /// Old stored JSON without this field deserializes to None.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,
    /// Informational synonyms (aliases) for this metric.
    /// Old stored JSON without this field deserializes to empty Vec.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub synonyms: Vec<String>,
    /// Access modifier: PUBLIC (default, queryable) or PRIVATE (hidden from queries,
    /// usable only in derived metric expressions).
    /// Old stored JSON without this field deserializes as Public.
    #[serde(default, skip_serializing_if = "AccessModifier::is_default")]
    pub access: AccessModifier,
```

5. **Add fields to Fact** (after output_type):
```rust
    /// Optional human-readable comment for this fact.
    /// Old stored JSON without this field deserializes to None.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,
    /// Informational synonyms (aliases) for this fact.
    /// Old stored JSON without this field deserializes to empty Vec.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub synonyms: Vec<String>,
    /// Access modifier: PUBLIC (default, queryable) or PRIVATE (hidden from queries,
    /// usable only in derived metric expressions).
    /// Old stored JSON without this field deserializes as Public.
    #[serde(default, skip_serializing_if = "AccessModifier::is_default")]
    pub access: AccessModifier,
```

6. **Add comment field to SemanticViewDefinition** (after schema_name):
```rust
    /// View-level comment describing the purpose of this semantic view.
    /// Old stored JSON without this field deserializes to None.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,
```

7. **Add `AccessModifier` to the model import in body_parser.rs**: The `use crate::model::{...}` line in body_parser.rs must later add `AccessModifier` -- but for Plan 01, this is not yet needed. The import will be added in Plan 02.

8. **Update any code that constructs these structs with struct literal syntax** -- any code that uses `TableRef { alias, table, pk_columns, unique_constraints }` without `..Default::default()` will fail to compile. Search for struct literals of TableRef, Dimension, Metric, Fact, and SemanticViewDefinition in body_parser.rs and parse.rs and add the new fields with default values. Specifically:
   - body_parser.rs line ~330: `Fact { name, expr, source_table, output_type: None }` -- add `comment: None, synonyms: vec![], access: AccessModifier::Public`
   - body_parser.rs line ~340: `Dimension { name, expr, source_table, output_type: None }` -- add `comment: None, synonyms: vec![]`
   - body_parser.rs line ~350: `Metric { name, expr, source_table, output_type: None, using_relationships }` -- add `comment: None, synonyms: vec![], access: AccessModifier::Public`
   - parse.rs line ~841: `SemanticViewDefinition { base_table, tables, dimensions, metrics, joins, facts, ... }` -- add `comment: None`

   For body_parser.rs, add `use crate::model::AccessModifier;` to the existing imports.

9. **Write tests** in src/model.rs `mod tests` section:
   - `pre_v060_json_deserializes_with_defaults` -- JSON blob WITHOUT comment/synonyms/access fields (matching v0.5.5 format) deserializes successfully. Assert: `def.comment.is_none()`, all dim.comment is None, dim.synonyms is empty, all met.comment is None, met.synonyms is empty, met.access == AccessModifier::Public, all fact.comment is None, fact.synonyms is empty, fact.access == AccessModifier::Public, all table.comment is None, table.synonyms is empty.
   - `access_modifier_default_is_public` -- `AccessModifier::default() == AccessModifier::Public`
   - `access_modifier_is_default` -- `Public.is_default() == true`, `Private.is_default() == false`
   - `metric_with_access_private_roundtrips` -- JSON with `"access":"Private"` deserializes to Private, serializes back with the field present.
   - `metric_with_access_public_omits_field` -- Serialize a Metric with access=Public and verify "access" key does NOT appear in JSON.
   - `dimension_with_comment_and_synonyms_roundtrips` -- Dimension with comment=Some("test") and synonyms=["a","b"] serializes and deserializes correctly.
   - `table_ref_with_metadata_roundtrips` -- TableRef with comment and synonyms roundtrips.
   - `fact_with_access_and_metadata_roundtrips` -- Fact with access=Private, comment, synonyms roundtrips.
   - `view_level_comment_roundtrips` -- SemanticViewDefinition with comment=Some("view comment") roundtrips.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test model::tests 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - src/model.rs contains `pub enum AccessModifier` with `#[default] Public` and `Private` variants
    - src/model.rs AccessModifier impl contains `pub fn is_default` returning `matches!(self, Self::Public)`
    - src/model.rs TableRef contains `pub comment: Option<String>` with `#[serde(default, skip_serializing_if = "Option::is_none")]`
    - src/model.rs TableRef contains `pub synonyms: Vec<String>` with `#[serde(default, skip_serializing_if = "Vec::is_empty")]`
    - src/model.rs Dimension contains `pub comment: Option<String>` and `pub synonyms: Vec<String>` with serde attrs
    - src/model.rs Metric contains `pub comment: Option<String>`, `pub synonyms: Vec<String>`, and `pub access: AccessModifier` with serde attrs
    - src/model.rs Fact contains `pub comment: Option<String>`, `pub synonyms: Vec<String>`, and `pub access: AccessModifier` with serde attrs
    - src/model.rs SemanticViewDefinition contains `pub comment: Option<String>` with `#[serde(default, skip_serializing_if = "Option::is_none")]`
    - `cargo test model::tests::pre_v060` exits 0
    - `cargo test model::tests::access_modifier` exits 0
    - `cargo test model::tests` exits 0 (all model tests pass)
    - body_parser.rs struct literal constructions compile (include new fields with defaults)
    - parse.rs struct literal construction of SemanticViewDefinition compiles (includes `comment: None`)
    - `cargo test` exits 0 (full cargo test suite passes -- no compilation errors from missing fields)
  </acceptance_criteria>
  <done>AccessModifier enum exists with Public/Private. All 5 model structs have metadata fields with correct serde attributes. Pre-v0.6.0 JSON deserializes with defaults. All new fields roundtrip correctly. Full cargo test suite passes.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| DDL text -> model structs | User-supplied SQL text parsed into typed Rust structs |
| model structs -> JSON persistence | Structs serialized to JSON and stored in DuckDB catalog table |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-43-01 | Tampering | model.rs serde | mitigate | All new fields use `#[serde(default)]` for backward compat; `skip_serializing_if` keeps JSON clean. No user input reaches model without parser validation (Plan 02). |
| T-43-02 | Denial of Service | COMMENT string length | accept | No explicit limit on comment length. DuckDB VARCHAR column storage handles size limits. Risk is low -- COMMENT is stored as a field in a JSON blob inside a single catalog row. |
</threat_model>

<verification>
```bash
cargo test model::tests 2>&1 | tail -20
cargo test 2>&1 | tail -5
```
All model tests pass. Full `cargo test` passes (no compilation errors from struct literal updates).
</verification>

<success_criteria>
- AccessModifier enum with Public (default) and Private variants exists in model.rs
- All 5 structs (TableRef, Dimension, Metric, Fact, SemanticViewDefinition) have metadata fields
- Metric and Fact have `access: AccessModifier` field
- All new fields use `#[serde(default)]` and appropriate `skip_serializing_if`
- Pre-v0.6.0 JSON (without any new fields) deserializes successfully
- All roundtrip tests pass
- `cargo test` passes with zero failures
</success_criteria>

<output>
After completion, create `.planning/phases/43-metadata-foundation/43-01-SUMMARY.md`
</output>
