---
phase: 64
plan: 02
type: execute
wave: 2
depends_on: [01]
files_modified:
  - src/parse.rs
  - src/query/table_function.rs
autonomous: true
requirements:
  - QID-01
  - QID-02
  - QID-03
  - QID-06
must_haves:
  truths:
    - "CREATE [OR REPLACE | IF NOT EXISTS] SEMANTIC VIEW with a fully-quoted FQN stores the bare unquoted last part as the catalog key"
    - "DROP / DESCRIBE / SHOW COLUMNS / ALTER capture sites accept the same quoted forms"
    - "ALTER ... RENAME TO normalises BOTH the source name AND the new-name target"
    - "Existence pre-check in emit_native_create_sql uses the NORMALISED name, not the raw token (defensive normalise at function entry is MANDATORY, not conditional)"
    - "semantic_view('\"orders_sv\"', ...) runtime arg also normalises to bare key"
    - "Error messages for missing/duplicate views reference the unquoted bare name"
    - "Delimiter scan for the name slot honours `\"...\"` regions so `\"my table\"` is captured intact"
  artifacts:
    - path: "src/parse.rs"
      provides: "Five capture sites + emit_native_create_sql delegate to ident::normalize_view_name + ident::find_identifier_end"
      contains: "ident::normalize_view_name"
    - path: "src/query/table_function.rs"
      provides: "Runtime semantic_view() positional arg normalised"
      contains: "normalize_view_name"
  key_links:
    - from: "src/parse.rs:extract_name_only"
      to: "src/ident.rs::normalize_view_name + find_identifier_end"
      via: "function call"
      pattern: "ident::normalize_view_name|crate::ident::normalize_view_name"
    - from: "src/parse.rs:rewrite_alter"
      to: "src/ident.rs::normalize_view_name (called TWICE — old name + RENAME TO target)"
      via: "function call"
      pattern: "normalize_view_name"
    - from: "src/parse.rs:validate_create_body"
      to: "src/ident.rs::normalize_view_name + find_identifier_end"
      via: "function call"
      pattern: "normalize_view_name|find_identifier_end"
    - from: "src/parse.rs:emit_native_create_sql"
      to: "src/ident.rs::normalize_view_name"
      via: "MANDATORY defensive shadowing at function entry — idempotent on already-normalised input from validate_create_body"
      pattern: "normalize_view_name"
    - from: "src/query/table_function.rs:bind"
      to: "src/ident.rs::normalize_view_name"
      via: "applied to bind.get_parameter(0)"
      pattern: "normalize_view_name"
---

<objective>
Wire `src/ident.rs` (built in 64-01) into every DDL identifier-capture site so that the bare unquoted last part becomes the lookup key, regardless of how the user wrote the name. This is the primary fix — quoted FQNs in CREATE / DROP / ALTER / DESCRIBE / SHOW COLUMNS now resolve uniformly.

Five capture sites in `src/parse.rs`:

1. **`extract_name_only`** at line 311 — DROP, DROP IF EXISTS, DESCRIBE, SHOW COLUMNS, ALTER source-name slot.
2. **`validate_create_body`** at line 1137 — CREATE, CREATE OR REPLACE, CREATE IF NOT EXISTS name capture.
3. **`extract_ddl_name`** at lines 793–796 (and 844 for the secondary path) — used by `extract_view_name_from_query` (consumed by `parse_function` / error-reporting).
4. **`rewrite_alter`** at line 634 — captures the source name (joins with `extract_name_only`-derived value from `rewrite_to_native_sql`) AND constructs the `RENAME TO` target.
5. **Existence pre-check in `emit_native_create_sql`** at line 1866 — MANDATORILY applies a defensive `normalize_view_name(&name)?` shadow at function entry, so the existence check is guaranteed to see the bare key irrespective of how the value reached this function.

Plus one runtime site:

6. **`src/query/table_function.rs:482`** — `bind.get_parameter(0)` reads the SQL string-literal contents of `semantic_view('...', ...)`. Normalise so `semantic_view('"orders_sv"', ...)` resolves the same as `semantic_view('orders_sv', ...)`.

Coverage: QID-01 (fully-quoted FQN CREATE), QID-02 (partial quoting CREATE), QID-03 (DROP/ALTER/DESCRIBE/SHOW COLUMNS + runtime arg), QID-06 (error messages reference unquoted name — implicit, since `name` IS the unquoted bare value).

Out of scope here: `quote_table_ref` re-quoting bug → 64-03. sqllogictest fixture + CHANGELOG + REQUIREMENTS append → 64-04.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/64-fix-quoted-identifier-handling/64-RESEARCH.md
@.planning/phases/64-fix-quoted-identifier-handling/64-01-SUMMARY.md
@./CLAUDE.md

<interfaces>
<!-- The helpers from 64-01 (src/ident.rs). Use these everywhere. -->

```rust
// src/ident.rs (created in 64-01)

pub fn parse_qualified_identifier(input: &str) -> Result<Vec<String>, String>;
pub fn normalize_view_name(input: &str) -> Result<String, String>;
pub fn find_identifier_end(input: &str, allow_paren: bool) -> usize;
```

<!-- The capture-site signatures we are modifying (current state from src/parse.rs). -->

```rust
// src/parse.rs:311  (extract_name_only)
fn extract_name_only(trimmed: &str, prefix_len: usize) -> Result<String, String>

// src/parse.rs:1124  (validate_create_body — name extraction segment)
//   ... `let name_end = after_prefix.find(|c: char| c.is_whitespace() || c == '(')` at line 1137 ...
//   returns Option<String> name via ParseError

// src/parse.rs:780  (extract_ddl_name / extract_view_name_from_query)
// src/parse.rs:625  (rewrite_alter — view name at line 634, RENAME TO new-name at lines 651+)
// src/parse.rs:1866 (emit_native_create_sql — `ctx.catalog.exists(name)`)

// src/query/table_function.rs:482  (bind)
let view_name = bind.get_parameter(0).to_string();
```

<!-- ParseError shape, for error path consistency. -->

```rust
// src/parse.rs (existing)
pub struct ParseError {
    pub message: String,
    pub position: Option<usize>,
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Wire ident helpers into the four parse.rs capture sites (extract_name_only, validate_create_body, extract_ddl_name, rewrite_alter)</name>
  <files>src/parse.rs</files>
  <read_first>
    - src/ident.rs (the helper module from 64-01 — confirm public signatures)
    - src/parse.rs lines 300–340 (extract_name_only)
    - src/parse.rs lines 780–860 (extract_ddl_name — both branches at lines 793 and 844)
    - src/parse.rs lines 625–710 (rewrite_alter — both the source-name slot AND the RENAME TO target slot)
    - src/parse.rs lines 1120–1170 (validate_create_body — name extraction segment around line 1137)
    - .planning/phases/64-fix-quoted-identifier-handling/64-RESEARCH.md §Pattern 2, §Pitfall 1, §Pitfall 2 (ALTER RENAME has TWO slots; SHOW COLUMNS/DESCRIBE share extract_name_only)
  </read_first>
  <behavior>
    For each of the four capture sites, the transformation is the SAME shape:

      Before (existing pattern):
        let name_end = after_prefix.find(|c: char| c.is_whitespace() || c == '(').unwrap_or(after_prefix.len());
        let name = &after_prefix[..name_end];

      After (post-fix):
        let name_end = crate::ident::find_identifier_end(after_prefix, /*allow_paren=*/true);  // false in name-only sites (no `(` for DROP/DESCRIBE/ALTER)
        let raw_name = &after_prefix[..name_end];
        let name = crate::ident::normalize_view_name(raw_name).map_err(|e| /* ParseError or String per the site */)?;

    Per-site rules:

    Site A — `extract_name_only` (line 311, returns Result<String, String>):
      - allow_paren = false (DROP / DESCRIBE / SHOW COLUMNS / ALTER all end at whitespace, never `(`)
      - On normalize_view_name Err, return `Err(format!("Invalid view name: {e}"))` — match existing message tone.

    Site B — `validate_create_body` name segment (line ~1137, returns Result<Option<String>, ParseError>):
      - allow_paren = true (CREATE ... AS ( ...) form has `(` legally after the name; but the SQL body parser uses `AS` keyword body now, so `(` is post-COMMENT/AS — the existing scan keeps `(` as a delimiter for safety)
      - On normalize_view_name Err, return `ParseError { message: format!("Invalid view name: {e}"), position: Some(trim_offset + plen) }`.

    Site C — `extract_ddl_name` (lines 793 and 844 — TWO sub-branches in the same function, one for CREATE forms at 793, one for the Drop/Describe/ShowColumns/Alter branch at 844 which routes via `extract_name_only`):
      - At line 793 (CREATE branch): same fix as Site B but the return type here is `Ok(Option<String>)` (the surrounding function returns name for error-reporting / observability). allow_paren = true.
      - At line 844 (other branch): already delegates to `extract_name_only` (Site A) — once Site A is fixed, Site C-second-branch is automatically fixed. Verify by reading.

    Site D — `rewrite_alter` (line 634 source-name, line 651+ RENAME TO target):
      - Source-name slot (after_prefix capture at line 633-636): apply the same delimiter scan replacement. allow_paren = false. On error, return `Err(...)` String.
      - **RENAME TO target slot**: locate the block (around `if rest_upper.starts_with("RENAME TO")` near line 651). Currently it captures `let new_name = rest["RENAME TO".len()..].trim();` and uses it raw. Replace with:
        - `let new_name_raw = rest["RENAME TO".len()..].trim();`
        - `let new_name = crate::ident::normalize_view_name(new_name_raw).map_err(|e| format!("Invalid new view name in RENAME TO: {e}"))?;`
        - Use `new_name` (the normalised String) wherever the old `new_name` (which was a &str) was consumed — adjust borrowing accordingly. Most consumers pass it into `escape_sql_arg` which takes `&str`, so a `.as_str()` or `&new_name` works.
      - Also normalise `view_name`/`safe_name` construction at lines 638–639: the variable was `view_name = &after_prefix[..name_end]` and then `safe_name = view_name.replace('\'', "''")`. After normalisation `view_name` is now an owned String — so do `let safe_name = view_name.replace('\'', "''")` against the owned String. (`view_name` is now `String`, was `&str`.)

    Tests (add to src/parse.rs's existing #[cfg(test)] mod tests):

    Required test cases (extend existing test modules — `grep -n "^mod tests\|#\[cfg(test)\]" src/parse.rs` to find the right module):

    Module `tests::extract_name_only_tests` (CREATE if not present):
      - drop_with_quoted_fqn: extract_name_only("DROP SEMANTIC VIEW \"db\".\"sch\".\"v\"", 19) -> "v"
      - drop_with_quoted_bare: ("DROP SEMANTIC VIEW \"orders_sv\"", 19) -> "orders_sv"
      - drop_with_unquoted_fqn: ("DROP SEMANTIC VIEW db.sch.v", 19) -> "v"
      - drop_with_quoted_whitespace_name: ("DROP SEMANTIC VIEW \"my view\"", 19) -> "my view"
      - error_unterminated_quote

    Module `tests::validate_create_body_tests` extend with:
      - create_with_quoted_fqn (full DDL including AS keyword body) — name lands as bare "orders_sv"
      - create_or_replace_with_quoted_fqn
      - create_if_not_exists_with_quoted_fqn
      - create_with_partial_quoting (`main."orders_sv"`)

    Module `tests::rewrite_alter_tests` extend with:
      - alter_rename_source_quoted: ALTER SEMANTIC VIEW "v" RENAME TO new_name → emitted SQL uses 'v' as the bare source name
      - alter_rename_target_quoted: ALTER SEMANTIC VIEW v RENAME TO "memory"."main"."new_v" → emitted SQL uses 'new_v' as the new bare name
      - alter_rename_both_quoted: both slots quoted FQN
      - alter_set_comment_quoted: ALTER SEMANTIC VIEW "v" SET COMMENT = 'x' → emitted SQL references 'v'

    Module `tests::extract_ddl_name_tests` (or whatever the existing surrounding suite is called):
      - quoted_fqn_extracted_as_bare
      - mixed_quoting_extracted_as_bare
  </behavior>
  <action>
    1. Read the four capture sites in full (use the line ranges in <read_first>).

    2. Apply the per-site transformations described in <behavior>:
       a. Site A: extract_name_only (line 311) — replace 4 lines (313–321 region) with the find_identifier_end + normalize_view_name pattern. allow_paren=false.
       b. Site B: validate_create_body name capture (line 1135–1140) — replace with allow_paren=true + normalize. Wrap error in ParseError with position.
       c. Site C: extract_ddl_name (line 793 area + line 844 area) — line 793 gets the full replacement; line 844 is already calling extract_name_only so verifying-only.
       d. Site D: rewrite_alter — TWO edits:
          - source-name slot at lines 633–639: replace the `name_end` scan + capture with find_identifier_end + normalize_view_name. Note `view_name` changes from `&str` to `String`.
          - RENAME TO target at lines 651–653 (locate by `rest["RENAME TO".len()..]`): wrap with normalize_view_name.
          - Ensure `safe_name` (`view_name.replace('\'', "''")`) still works against the now-owned String. (Strings have `replace` too — should be fine but verify build.)

    3. Add the test cases in <behavior> to the appropriate test modules. If a target test module doesn't exist, create it next to existing modules — DON'T scatter tests across the file.

    4. Run `cargo test --lib parse::tests` and verify all new + pre-existing tests pass.

    5. Run `cargo test --lib` (whole suite) — verify no regressions outside parse::tests.

    Style note: import the helpers at the top of `src/parse.rs` with `use crate::ident::{find_identifier_end, normalize_view_name};` so the call sites stay readable as `normalize_view_name(...)` rather than `crate::ident::normalize_view_name(...)`. The existing file already uses bare-name imports for `crate::expand::resolution::quote_ident` etc., so match that convention.

    Do NOT modify `emit_native_create_sql` in this task — that's Task 2 of this plan, so we keep the diff reviewable per-task.
  </action>
  <verify>
    <automated>cargo test --lib parse:: -- --nocapture 2>&1 | tail -30</automated>
  </verify>
  <acceptance_criteria>
    - `grep -n "use crate::ident::" src/parse.rs` returns one match at the top of the file.
    - `grep -cE "normalize_view_name\(" src/parse.rs` returns at least 5 (one per capture site + RENAME target).
    - `grep -cE "find_identifier_end\(" src/parse.rs` returns at least 4 (one per non-extract_name_only capture site that still does its own delimiter scan, plus extract_name_only itself).
    - `grep -n "\.find(|c: char| c.is_whitespace()" src/parse.rs` returns AT MOST the count BEFORE this change MINUS 4 (the four sites we replaced). Acceptable remaining `is_whitespace` calls: lines around 470, 499, 556, 592, 1020, 2309, 2378 — those are NOT identifier-capture sites; document if uncertain.
    - `cargo test --lib parse::` exits 0.
    - `cargo test --lib` exits 0 (full suite — sqllogictest gate comes in 64-04).
    - At least eight new test cases land (DROP / DESCRIBE-equivalent / CREATE × 3 forms / ALTER source-quoted / ALTER target-quoted / ALTER both-quoted).
  </acceptance_criteria>
  <done>
    Four capture sites in src/parse.rs delegate to ident::normalize_view_name + ident::find_identifier_end. Quoted CREATE/DROP/ALTER/DESCRIBE/SHOW COLUMNS all store / look up the bare name. RENAME TO target normalised. cargo test --lib green.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Mandatory defensive normalise at emit_native_create_sql entry + runtime semantic_view() arg in table_function.rs</name>
  <files>src/parse.rs, src/query/table_function.rs</files>
  <read_first>
    - src/parse.rs lines 1855–1900 (emit_native_create_sql — the existence pre-check around line 1866)
    - src/parse.rs lines 1900–1950 (continuation — see how `name`, `name_escaped` flow into INSERT/error messages)
    - src/query/table_function.rs lines 470–510 (bind function — `let view_name = bind.get_parameter(0).to_string();`)
    - src/ident.rs (helpers — confirm signatures)
    - .planning/phases/64-fix-quoted-identifier-handling/64-RESEARCH.md §Pitfall 3 (existence pre-check), §Open Question 2 (runtime arg normalisation)
  </read_first>
  <behavior>
    Site E — emit_native_create_sql defensive normalise (src/parse.rs:1866):

    The fix from Task 1 (Site B in validate_create_body) ALREADY normalises `name` before it reaches emit_native_create_sql in the happy-path call chain. However, to harden against any future regression or alternative call path that might pass a raw token, this task ALWAYS adds an unconditional defensive shadow at function entry.

    **MANDATORY** — at the top of `emit_native_create_sql`, immediately after the existing argument-handling preamble and BEFORE the first use of `name`, add:

    ```rust
    /// Defensive normalisation — idempotent on already-normalised input from validate_create_body.
    /// Guarantees `name` is the bare view identifier byte-for-byte matching the catalog PK,
    /// regardless of how this function was called. Cost: one strdup + state-machine pass
    /// (~tens of ns on typical view names) — negligible relative to the catalog INSERT this
    /// function performs.
    let name = crate::ident::normalize_view_name(&name).map_err(|e| ParseError {
        message: format!("Invalid view name: {e}"),
        position: None,
    })?;
    ```

    Notes on the shadow:
    - `name` is the function parameter (usually `&str` or `String` — read the signature and adapt accordingly).
    - If the parameter is already `String`, `normalize_view_name(&name)` works (it takes `&str`).
    - If the parameter is `&str`, `normalize_view_name(name)` works directly.
    - Either way, the shadowed `name` becomes an owned `String` after the shadow. All subsequent uses (including `name_escaped`, error message formatting, INSERT-statement generation) continue to compile because `String` auto-derefs to `&str` where needed.
    - This is unconditional — do NOT gate on a trace of the call sites. The cost is negligible; the safety guarantee is worth more.

    Then ALSO add a documentation comment above the existence-check block (around line 1865) stating:
    `// `name` is guaranteed to be the bare view identifier — defensively normalised at function entry. The catalog row's PK column stores the same bare value, so `ctx.catalog.exists(name)` is a byte-for-byte match.`

    Site F — runtime semantic_view() arg (src/query/table_function.rs:482):

    Current code:
      let view_name = bind.get_parameter(0).to_string();

    Fix:
      let view_name_raw = bind.get_parameter(0).to_string();
      let view_name = crate::ident::normalize_view_name(&view_name_raw)
          .map_err(|e| format!("Invalid view name '{view_name_raw}': {e}"))?;

    The function return type is `Result<Self::BindData, Box<dyn std::error::Error>>` — `String` converts via `From<String> for Box<dyn Error>`, so `.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?` may be needed depending on Rust's inference. Test the compile.

    Tests (parse.rs side — module `tests::emit_existence_check_uses_bare_name_tests` or similar):
      - exists_check_with_quoted_create — call validate_and_rewrite on `CREATE SEMANTIC VIEW "v" AS TABLES (...)` (full minimal body); inject a fake catalog with `v` already present; assert error message is `semantic view 'v' already exists; ...` (NOT `semantic view '"v"' ...`).
      - emit_native_create_sql_defensive_normalise_idempotent — call the function (if it's `pub` or test-visible) with an already-bare name; assert no panic, no error, output unchanged. (If the function is private, skip this test and rely on the upstream behavioural test above; document.)
      - (If the test infrastructure makes catalog-injection awkward — i.e., ctx.catalog is hard to fake — instead pair with a sqllogictest case in 64-04. Document in <action> which path was chosen.)

    Tests (table_function.rs side — module `tests::view_name_normalisation_tests`):
      - Cannot unit-test bind() directly (it requires BindInfo, which only exists at runtime). Document this and rely on sqllogictest coverage in 64-04 (`semantic_view('"orders_sv"', ...)` resolves).
  </behavior>
  <action>
    1. Open `src/parse.rs`. Locate `emit_native_create_sql` (around line 1860).

    2. Apply the MANDATORY defensive normalise at function entry per <behavior> Site E. This is unconditional — do NOT trace call sites first and skip the shadow if the trace shows post-normalisation input. Always add the shadow. Always add the doc-comment above the existence check.

    3. Verify the shadow compiles — `name` type may need an `&` or `.as_str()` adjustment depending on whether the parameter is `&str` or `String`. Read the signature and adapt.

    4. Open `src/query/table_function.rs`. Apply the Site F fix:
       - Locate line 482 (`let view_name = bind.get_parameter(0).to_string();`).
       - Replace with the two-line pattern under <behavior>.
       - Add a brief doc-comment: `// Normalise quoted/qualified forms (e.g. semantic_view('"v"', ...)) to bare key.`
       - Ensure the function compiles — adjust the `Box<dyn Error>` conversion as needed.

    5. Run `cargo test --lib` — confirm no regressions.

    6. If feasible, add a unit test in parse.rs that proves the existence-check uses the bare name (depends on catalog test infrastructure). If not feasible, add a TODO comment referencing the 64-04 sqllogictest fixture and move on.

    7. Run the post-edit grep self-check:
       - `grep -n "normalize_view_name" src/parse.rs` MUST return at least 6 matches (5 capture sites from Task 1 + 1 defensive shadow at emit_native_create_sql entry from Task 2). If it returns fewer, the defensive shadow was not added — fix and re-run.
  </action>
  <verify>
    <automated>cargo test --lib -- --nocapture 2>&1 | tail -40</automated>
  </verify>
  <acceptance_criteria>
    - `grep -n "Defensive normalisation" src/parse.rs` returns one match (the doc-comment on the defensive shadow in emit_native_create_sql).
    - `grep -nE "// .*bare view identifier|// .*defensively normalised" src/parse.rs` returns at least one comment near line 1865.
    - `grep -cE "normalize_view_name\(" src/parse.rs` returns at least 6 (five capture sites from Task 1 + one defensive shadow at emit_native_create_sql entry from Task 2).
    - `grep -n "normalize_view_name" src/query/table_function.rs` returns one match.
    - `grep -B1 -A2 "bind.get_parameter(0)" src/query/table_function.rs` shows the normalisation immediately after.
    - `cargo build --lib` succeeds.
    - `cargo test --lib` exits 0.
    - `cargo test --lib --features extension` (or whatever the build feature is — check Cargo.toml) — verify the extension-feature compile still works since src/query/table_function.rs is in the extension code path. If feature-gating causes issues, gate the normalize call with the same `#[cfg(feature = "extension")]` as the surrounding code; document.
  </acceptance_criteria>
  <done>
    emit_native_create_sql carries a MANDATORY defensive `normalize_view_name` shadow at function entry (idempotent on already-normalised input) plus a doc-comment confirming the bare-key invariant. The runtime semantic_view() positional arg in src/query/table_function.rs:482 is normalised. Full cargo test suite green. `grep -c "normalize_view_name" src/parse.rs` ≥ 6.
  </done>
</task>

</tasks>

<verification>
- `cargo test --lib` green.
- `grep -cE "normalize_view_name" src/parse.rs` >= 6 (5 capture sites + 1 defensive shadow); `grep -c "normalize_view_name" src/query/table_function.rs` == 1.
- No `find(|c: char| c.is_whitespace())` calls remain in the four identifier-capture sites (lines 311, 633-636, 793-796, 1136-1138 region — replaced by find_identifier_end).
- Files modified: exactly two (`src/parse.rs`, `src/query/table_function.rs`).
- No edits to `src/expand/resolution.rs` (that's 64-03).
- No edits to test/sql/ (that's 64-04).
</verification>

<success_criteria>
- All five `src/parse.rs` capture sites delegate to `ident::normalize_view_name` (four direct + one transitively via the SHOW COLUMNS/DESCRIBE/ALTER-source path through extract_name_only).
- ALTER RENAME TO normalises BOTH slots.
- `emit_native_create_sql` carries the mandatory defensive shadow — even if a future caller passes a raw token, the existence check sees the bare key.
- Runtime `semantic_view('"...", ...)` arg normalised in `src/query/table_function.rs`.
- At least eight new unit tests covering quoted forms across CREATE / DROP / DESCRIBE / ALTER.
- `cargo test --lib` green.
- No changes to expansion path (`src/expand/`) — that's 64-03's exclusive territory, so this plan can run in parallel with 64-03 (Wave 2).
</success_criteria>

<output>
After completion, create `.planning/phases/64-fix-quoted-identifier-handling/64-02-SUMMARY.md` recording:

- Each of the five capture-site call sites with the exact line numbers POST-edit.
- The exact line number of the mandatory defensive shadow in `emit_native_create_sql` (proves the unconditional fix landed).
- Number of new unit tests added per module.
- Any feature-gating tweaks needed for src/query/table_function.rs.
- The final `grep -c "normalize_view_name" src/parse.rs` count (must be ≥ 6).
</output>
