---
phase: 67-expansion-sql-coverage-and-tech-debt-cleanup
plan: 02
type: execute
wave: 2
depends_on: ["67-01"]
files_modified:
  - src/body_parser.rs
  - test/sql/phase67_quoted_source_tables.test
  - test/sql/TEST_LIST
  - TECH-DEBT.md
autonomous: true
requirements: []
must_haves:
  truths:
    - "`src/body_parser.rs::parse_single_table_entry` no longer splits the source-table-name slot on raw whitespace — it uses `src/ident.rs::find_identifier_end` to advance past each identifier (including quoted identifiers with internal whitespace) before continuing tokenization."
    - "`src/ident.rs` is NOT modified — the existing `find_identifier_end` helper (shipped in Phase 64) is reused as-is; this plan is import + call-site surgery only on the body parser side."
    - "A new sqllogictest fixture exercises at least one source-table name with internal whitespace (e.g. `\"my db\".\"schema\".\"t\"`) via a full CREATE → query → expansion roundtrip, proving the fix end-to-end. Default per D-11: separate `test/sql/phase67_quoted_source_tables.test` file; folding into `phase67_qualified_emission.test` (Plan 01) is acceptable if the executor chooses."
    - "An audit-grep pass against `src/body_parser.rs` searches for OTHER `split_whitespace` / `split(' ')` call sites that could harbor the same bug (D-10). Findings are either fixed in-phase (if mechanical) or surfaced as TECH-DEBT additions (if structural). Default expectation per CONTEXT.md: the one site SCOPE.md identifies is the only one — but the grep MUST run and its result MUST be recorded in the SUMMARY."
    - "TECH-DEBT.md entry #24 is marked RESOLVED (commit SHA + phase reference inline) — not deleted, but the `❓` status flips to ✅ or the entry is moved to a 'Resolved' section per repo convention."
    - "`just test-all` exits 0 — Rust unit + proptest + sqllogictest (with new fixture) + DuckLake CI. This plan touches production code (`src/body_parser.rs`), so the full quality gate is required, not just `just test-sql`."
    - "Pre-existing TABLES-clause parsing behaviour (alias, AS keyword, unquoted physical table names, multi-part qualified names without whitespace, PRIMARY KEY / UNIQUE trailing clauses) is byte-for-byte preserved — all 47 existing sqllogictest fixtures still pass; all 838+ cargo unit tests still pass."
  artifacts:
    - path: "src/body_parser.rs"
      provides: "Identifier-aware source-table tokenisation in parse_single_table_entry using find_identifier_end"
      contains: "find_identifier_end"
    - path: "test/sql/phase67_quoted_source_tables.test"
      provides: "Regression test for `TABLES (o AS \"my db\".\"schema\".\"t\" PRIMARY KEY (id))` style entries — full CREATE → query → expansion roundtrip"
      contains: "my orders"
    - path: "test/sql/TEST_LIST"
      provides: "Registration entry `phase67_quoted_source_tables.test`"
      contains: "phase67_quoted_source_tables.test"
    - path: "TECH-DEBT.md"
      provides: "Entry #24 marked RESOLVED with Phase 67 reference"
      contains: "Phase 67"
  key_links:
    - from: "src/body_parser.rs::parse_single_table_entry"
      to: "src/ident.rs::find_identifier_end"
      via: "use crate::ident::find_identifier_end + call at the source-table-name tokenisation site (replacing the existing whitespace tokenisation)"
      pattern: "find_identifier_end\\("
    - from: "test/sql/phase67_quoted_source_tables.test"
      to: "src/body_parser.rs (fixed parser)"
      via: "CREATE SEMANTIC VIEW with quoted source-table name containing internal whitespace parses successfully and produces the expected qualified-FROM expansion"
      pattern: "TABLES \\(.*AS \"[a-z ]+ [a-z]+\""
    - from: "TECH-DEBT.md #24"
      to: "Phase 67 Plan 02 commit"
      via: "inline resolution annotation"
      pattern: "Resolved in Phase 67"
---

<objective>
Close TECH-DEBT entry #24: `src/body_parser.rs::parse_single_table_entry` currently tokenises the `TABLES (...)` clause source-table-name slot on whitespace, breaking entries like `TABLES (o AS "my db"."schema"."t" PRIMARY KEY (id))`. This plan ports `src/ident.rs::find_identifier_end` (shipped in Phase 64 for the same identifier-aware tokenisation problem at every OTHER capture site — CREATE / DROP / ALTER / DESCRIBE / SHOW COLUMNS / expansion-side `quote_table_ref`) to the one remaining body-parser site that Phase 64 missed.

Purpose: Phase 64 completed identifier-aware handling at every capture site EXCEPT this one, leaving a documented but unfixed parse failure for quoted source-table names with internal whitespace. The fix unblocks a real user-facing parse failure (rare in practice, but the path exists and the workaround — DuckDB-side `CREATE VIEW` aliasing — is documented in TECH-DEBT.md). Mechanically: the fix is a single-helper-reuse, not a new identifier parser. The audit-grep ensures we don't leave a sibling bug unfixed.

Output:
- Single-site surgery in `src/body_parser.rs::parse_single_table_entry` swapping the whitespace tokenisation for `find_identifier_end`-driven advancement
- Audit-grep pass over `src/body_parser.rs` for OTHER `split_whitespace` / `split(' ')` call sites in identifier slots — findings either fixed in-phase or surfaced as TECH-DEBT additions
- New `test/sql/phase67_quoted_source_tables.test` exercising the fix end-to-end via CREATE + query + explain_semantic_view
- TEST_LIST registration
- TECH-DEBT.md #24 marked RESOLVED
- No changes to `src/ident.rs` (per D-09)
</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/67-expansion-sql-coverage-and-tech-debt-cleanup/67-CONTEXT.md
@.planning/phases/67-expansion-sql-coverage-and-tech-debt-cleanup/SCOPE.md

# Source of truth — the helper to be reused (DO NOT modify)
@src/ident.rs

# Surgery site
@src/body_parser.rs

# Tech debt source
@TECH-DEBT.md

# Project rules — Rules 1 & 2 non-negotiable; pre-commit fmt+clippy
@CLAUDE.md

<interfaces>
<!-- Helper to reuse (src/ident.rs:179-186 — DO NOT modify) -->

```rust
/// Returns the byte index where the identifier ending at the start of `input` ends.
/// Handles quoted identifiers ("...") with embedded whitespace; unquoted identifiers stop at
/// whitespace, comma, paren, or dot (depending on `allow_paren`).
///
/// # Examples
/// assert_eq!(find_identifier_end("orders_sv AS ...", true), 9);
/// assert_eq!(find_identifier_end("\"my table\" AS ...", true), 10);
/// assert_eq!(find_identifier_end("v(foo)", true), 1);
/// assert_eq!(find_identifier_end("v(foo)", false), 6);
pub fn find_identifier_end(input: &str, allow_paren: bool) -> usize;
```

<!-- Current surgery site (src/body_parser.rs:660-770, lines around `parse_single_table_entry`) -->
<!-- Existing flow after consuming the alias + AS keyword: -->
<!--   let upper = after_as.to_ascii_uppercase(); -->
<!--   let pk_pos = find_primary_key(&upper); -->
<!--   if let Some((pk_start, pk_end)) = pk_pos { let table_name = after_as[..pk_start].trim(); ... } -->
<!-- The byte range `after_as[..pk_start]` assumes the source-table name is everything up to the PRIMARY KEY keyword. -->
<!-- The bug: this DOES find PRIMARY KEY correctly even with quoted-whitespace names, BUT the surrounding -->
<!-- tokenisation (alias, AS) uses split_first_token which IS whitespace-based — and the upstream call sites -->
<!-- in the file (lines ~1391 and ~1707) also use split_whitespace for analogous identifier tokenisation. -->
<!-- The full bug surface: the SOURCE-TABLE-NAME slot capture is robust against internal whitespace today -->
<!-- ONLY because find_primary_key happens to be a substring search; if the user writes -->
<!-- TABLES (o AS "weird PRIMARY KEY name".t PRIMARY KEY (id)) the substring search would match the WRONG -->
<!-- PRIMARY KEY. The conservative fix: use find_identifier_end to advance past the source-table name -->
<!-- FIRST, then run find_primary_key only on the post-name slice. -->

<!-- Target shape (pseudocode): -->

```rust
// AFTER `let after_as = rest[2..].trim_start();` (line 680):
// Walk through dot-separated identifier components using find_identifier_end.
let mut pos = 0usize;
loop {
    let segment_end = find_identifier_end(&after_as[pos..], /* allow_paren = */ false);
    if segment_end == 0 {
        return Err(ParseError {
            message: format!("Missing physical table name after AS for alias '{alias}' in TABLES clause."),
            position: Some(after_as_offset + pos),
        });
    }
    pos += segment_end;
    let rest_after_segment = &after_as[pos..];
    if rest_after_segment.starts_with('.') {
        pos += 1; // consume the dot, continue to next segment
        continue;
    }
    break;
}
let table_name = &after_as[..pos];
let after_table = after_as[pos..].trim_start();
// Then run find_primary_key on `after_table` (NOT on `after_as`) for the PK detection.
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Apply find_identifier_end at parse_single_table_entry source-table-name capture site</name>
  <files>src/body_parser.rs</files>
  <read_first>
    - src/body_parser.rs:1-30 (existing imports — verify whether `crate::ident::*` is already imported; if not, add `use crate::ident::find_identifier_end;`)
    - src/body_parser.rs:640-770 (full `parse_table_entries_inner` + `parse_single_table_entry` function — understand the existing flow before editing; specifically the find_primary_key + after_as[..pk_start] logic at lines 683-740)
    - src/body_parser.rs:1380-1410, 1700-1730 (the OTHER `split_whitespace` sites surfaced by grep — Task 2 audits these, not this one, but reading them now confirms the surgery is scoped correctly)
    - src/ident.rs:179-186 (helper definition — DO NOT modify, only call)
    - src/ident.rs:399-450 (existing unit tests covering `find_identifier_end` for quoted-with-whitespace, dot-separated, unterminated quote — confirms the helper handles the cases we need)
    - TECH-DEBT.md entry #24 (the bug description + workaround that the fix makes obsolete)
  </read_first>
  <behavior>
    - Input: `TABLES (o AS "my orders" PRIMARY KEY (id))`
      Expected: `parse_single_table_entry` extracts `table_name = "\"my orders\""`, `alias = "o"`, `pk_columns = vec!["id"]`. Pre-fix behaviour: the alias-tokeniser at line 663 (`split_first_token`) returns `alias = "o"` correctly, but the source-table-name slice at line 688 (`after_as[..pk_start].trim()`) DOES include the full `"my orders"` because `find_primary_key` scans for the literal `PRIMARY KEY` substring which is unaffected by internal whitespace. The bug is more subtle than the basic case suggests — see Input 2.
    - Input: `TABLES (o AS "weird PRIMARY KEY name" PRIMARY KEY (id))`
      Expected with fix: `table_name = "\"weird PRIMARY KEY name\""`, `pk_columns = vec!["id"]`.
      Pre-fix behaviour: `find_primary_key` matches the FIRST occurrence of `PRIMARY KEY` inside the quoted name, truncates the source-table name to `"weird`, and tries to parse a malformed PK clause from the rest. This is the canonical bug-class TECH-DEBT #24 calls out.
    - Input: `TABLES (o AS "my db"."schema"."my table" PRIMARY KEY (id))` (3-part quoted FQN with internal whitespace in two segments)
      Expected with fix: `table_name = "\"my db\".\"schema\".\"my table\""`, `pk_columns = vec!["id"]`.
    - Input: `TABLES (o AS schema.t)` (no PK, no whitespace, unqualified-ish — existing happy path)
      Expected: byte-identical to pre-fix output (`table_name = "schema.t"`, `alias = "o"`, no PK columns).
    - Input: `TABLES (o AS t UNIQUE (id))` (UNIQUE-only, no PK)
      Expected: byte-identical to pre-fix (`table_name = "t"`, `alias = "o"`, UNIQUE handled by the existing post-table-name logic).
    - All 47 existing sqllogictest fixtures and all 838+ cargo unit tests continue to pass.
  </behavior>
  <action>
1. Add or extend the import at the top of `src/body_parser.rs`: `use crate::ident::find_identifier_end;` (verify via `grep -n "^use crate::ident" src/body_parser.rs` whether `crate::ident::*` is already imported; if so, add `find_identifier_end` to that use list — don't duplicate the import line).

2. In `parse_single_table_entry` (starts at line 660), locate the block at lines 683-715 that does:
   - `let upper = after_as.to_ascii_uppercase();`
   - `let pk_pos = find_primary_key(&upper);`
   - `if let Some((pk_start, pk_end)) = pk_pos { let table_name = after_as[..pk_start].trim(); ... }`

3. Restructure to FIRST consume the source-table name using `find_identifier_end`-driven advancement across dot-separated segments, THEN run `find_primary_key` / UNIQUE detection on the REMAINING slice. The replacement walks segment-by-segment as shown in the `<interfaces>` pseudocode above.

4. After the new identifier-aware capture (`table_name` now correctly captures multi-part quoted FQNs with internal whitespace), run the EXISTING PRIMARY KEY / UNIQUE detection on `after_table` (the slice AFTER the name), not on `after_as` (the slice from AS onward). Adjust the byte offsets returned by `find_primary_key` to be relative to `after_table`, then translate back to `after_as` coordinates as needed for error position reporting (the `after_as_offset` shift).

5. Preserve all existing semantics byte-for-byte for the non-bug cases:
   - Empty table_name after AS → error with existing message wording.
   - PRIMARY KEY parens balanced via `extract_paren_content` — unchanged.
   - UNIQUE clauses (one or more) trailing the PK clause — unchanged.
   - No PK, no UNIQUE → bare table name path (the `else` branch in the current code at the `if let Some((pk_start, pk_end)) = pk_pos` matcher) — apply the same identifier-aware capture here too.

6. Add new Rust-side unit tests inside `#[cfg(test)] mod tests` covering the new cases enumerated in `<behavior>`:
   - `test_parse_single_table_entry_quoted_with_internal_whitespace` (Input 1)
   - `test_parse_single_table_entry_quoted_containing_primary_key_substring` (Input 2 — the canonical TECH-DEBT #24 bug)
   - `test_parse_single_table_entry_3part_quoted_fqn_with_whitespace` (Input 3)
   - `test_parse_single_table_entry_regression_no_whitespace` (Input 4 — proves no regression on the happy path)
   Each test asserts the extracted `table_name`, `alias`, `pk_columns` against expected values.

7. After the surgery, run `cargo test -p semantic-views --lib` and `cargo clippy --all-targets -- -D warnings` and `cargo fmt --check`. Pre-commit hook runs fmt-check + clippy. If commit fails on fmt-check, run `cargo fmt`, re-stage, retry. NEVER use `--no-verify` (CLAUDE.md).
  </action>
  <verify>
    <automated>cargo test -p semantic-views --lib > /tmp/claude/cargo_test_67_02_task1.log 2>&1; RC=$?; tail -80 /tmp/claude/cargo_test_67_02_task1.log; test "$RC" -eq 0 && cargo clippy --all-targets -- -D warnings > /tmp/claude/clippy_67_02_task1.log 2>&1; RC=$?; tail -40 /tmp/claude/clippy_67_02_task1.log; test "$RC" -eq 0 && cargo fmt --check > /tmp/claude/fmt_67_02_task1.log 2>&1; RC=$?; tail -20 /tmp/claude/fmt_67_02_task1.log; test "$RC" -eq 0</automated>
  </verify>
  <acceptance_criteria>
    - `grep -n "find_identifier_end" src/body_parser.rs` returns at least one match inside `parse_single_table_entry` (the new call site).
    - `grep -n "use crate::ident" src/body_parser.rs` includes `find_identifier_end` (either as the only item or in a multi-item use list).
    - At least 4 new unit tests inside `#[cfg(test)] mod tests` exercising the cases in `<behavior>` exist (`grep -c "test_parse_single_table_entry_" src/body_parser.rs` returns >= 4 more than baseline).
    - `cargo test -p semantic-views --lib` exits 0 — all 838+ unit tests + the 4 new ones pass.
    - `cargo clippy --all-targets -- -D warnings` exits 0.
    - `cargo fmt --check` exits 0.
    - If proptests exist over the TABLES clause, they exit 0.
  </acceptance_criteria>
  <done>Single-site surgery applied; `find_identifier_end` consumed at the source-table-name capture; 4+ new unit tests prove the fix; existing tests green; clippy + fmt clean.</done>
</task>

<task type="auto">
  <name>Task 2: Audit-grep pass for sibling whitespace-tokenisation bugs in body_parser.rs (D-10)</name>
  <files>(no files modified during the grep itself; if findings surface, edits land in this task)</files>
  <read_first>
    - src/body_parser.rs (whole file — focus on lines flagged by the grep below; previously known: lines 1391 and 1707 use `split_whitespace`)
    - 20-line context around each grep hit to classify the call site
  </read_first>
  <action>
1. Run: `grep -n "split_whitespace\|split(.\047 .\047)" src/body_parser.rs > /tmp/claude/body_parser_audit_grep_67_02.txt; cat /tmp/claude/body_parser_audit_grep_67_02.txt`

2. For each match, classify into one of:
   - (a) Identifier slot (e.g. column name, table name, alias) — bug equivalent to TECH-DEBT #24; FIX IN-PHASE.
   - (b) Keyword-only slot (e.g. tokenising `AS`, `PRIMARY KEY`, `UNIQUE`, `NULLS LAST` — keywords by definition have no internal whitespace; CORRECT-BY-CONSTRUCTION, no fix).
   - (c) User-expression slot (e.g. dimension `expr` value passed through to DuckDB intact) — verify by tracing the slot through to its consumer; if body_parser actually parses it, classify as new TECH-DEBT addition.

3. For each (a)-class finding beyond the one fixed in Task 1: fix in-phase using the same `find_identifier_end`-driven pattern. Repeat the unit-test + clippy + fmt cycle after each fix.

4. For each (b)-class finding: document in the SUMMARY that the site is correct-by-construction; do NOT fix.

5. For each (c)-class finding that turns out to be a real bug AND non-mechanical to fix: append a new entry to TECH-DEBT.md with the same shape as entry #24 (Origin, Decision, Why deferred, Action if a user hits this). Do NOT fix in-phase — that's structural scope creep per CONTEXT.md `<risk_areas>`.

6. Default expectation per CONTEXT.md D-10: only the one Task 1 site is a real bug. Lines 1391 and 1707 are most likely keyword tokenisation in DIMENSION / METRIC entry parsing where `AS` is the separator. If that's confirmed, write in the SUMMARY: "Audit grep ran; lines 1391/1707 confirmed (b)-class keyword tokenisation, not identifier tokenisation; no additional surgery needed."

7. Record the full audit result inline in the eventual `67-02-SUMMARY.md`: list every `split_whitespace` site with file:line, classification (a/b/c), and disposition (fixed-in-phase / not-a-bug / new-tech-debt).
  </action>
  <verify>
    <automated>grep -n "split_whitespace\|split(.\047 .\047)" src/body_parser.rs > /tmp/claude/body_parser_audit_grep_67_02.txt; cat /tmp/claude/body_parser_audit_grep_67_02.txt; echo "Audit complete; disposition to be recorded in SUMMARY"; if git diff --name-only HEAD src/body_parser.rs | grep -q body_parser.rs; then echo "Additional surgery detected — running cargo check + body_parser unit tests"; cargo check > /tmp/claude/cargo_check_67_02_task2.log 2>&1; RC=$?; tail -40 /tmp/claude/cargo_check_67_02_task2.log; test "$RC" -eq 0 && cargo test -p semantic-views --lib body_parser > /tmp/claude/cargo_test_67_02_task2.log 2>&1; RC=$?; tail -60 /tmp/claude/cargo_test_67_02_task2.log; test "$RC" -eq 0; else echo "No additional surgery applied in Task 2 beyond Task 1; gate is a no-op per Issue 4 conditional"; fi</automated>
  </verify>
  <acceptance_criteria>
    - The audit-grep output is captured at `/tmp/claude/body_parser_audit_grep_67_02.txt` and referenced by line number in the SUMMARY.
    - Every `split_whitespace` site in `src/body_parser.rs` has a documented classification (a/b/c) and disposition.
    - If any (a)-class findings emerged beyond Task 1: `cargo test -p semantic-views --lib` + clippy + fmt all still exit 0 after the additional fixes.
    - If no additional findings: SUMMARY explicitly states the grep ran, the sites are (b)/(c)-class non-bugs, and the one site at line ~661 (Task 1) is the only one fixed.
  </acceptance_criteria>
  <done>Audit-grep complete; every site classified; either fixed-in-phase or surfaced as new TECH-DEBT; SUMMARY records disposition.</done>
</task>

<task type="auto">
  <name>Task 3: Create phase67_quoted_source_tables.test + register in TEST_LIST (D-11)</name>
  <files>test/sql/phase67_quoted_source_tables.test, test/sql/TEST_LIST</files>
  <read_first>
    - test/sql/phase64_quoted_idents.test (closest analog — Phase 64 fixture exercising quoted view-name handling; mirror its structure)
    - test/sql/TEST_LIST (read the whole file; confirm format and pick an insertion point near other phase67_*.test entries)
    - src/body_parser.rs (the fixed `parse_single_table_entry` — confirm the fixture exercises the exact path Task 1 fixed)
    - CLAUDE.md (sqllogictest rules — `statement error` block-form, no inline regex; TEST_LIST registration mandatory)
    - 67-CONTEXT.md D-11 (fixture location decision; default is separate file)
  </read_first>
  <action>
Create `test/sql/phase67_quoted_source_tables.test` with the following structure.

1. **Header comment** citing CONTEXT.md D-09..D-11, TECH-DEBT.md #24, the fix commit (Plan 02 Task 1).

2. **`LOAD semantic_views;`**

3. **Scenario 1 — Quoted source-table name with internal whitespace, full roundtrip**:
   - `CREATE TABLE "my orders" (id INTEGER PRIMARY KEY, region VARCHAR, amount DECIMAL(18,2));`
   - `INSERT INTO "my orders" VALUES (1, 'US', 100.00), (2, 'EU', 200.00);`
   - `CREATE SEMANTIC VIEW p67_quoted_src AS TABLES (o AS "my orders" PRIMARY KEY (id)) DIMENSIONS (o.region AS o.region) METRICS (o.total AS SUM(o.amount));`
   - Smoke check: `SELECT count(*) > 0 FROM semantic_view('p67_quoted_src', dimensions := ['region'], metrics := ['total'])` returns `true`.
   - Shape assertion: `SELECT count(*) > 0 FROM explain_semantic_view('p67_quoted_src', dimensions := ['region'], metrics := ['total']) WHERE explain_output LIKE '%FROM "memory"."main"."my orders"%'` returns `true`. (The internal whitespace must be preserved through CREATE → expansion emission.)

4. **Scenario 2 — Multi-part quoted FQN with internal whitespace in two segments**:
   - `CREATE SCHEMA "stage 2";`
   - `CREATE TABLE "stage 2"."my orders" (id INTEGER PRIMARY KEY, region VARCHAR, amount DECIMAL(18,2));`
   - `INSERT INTO "stage 2"."my orders" VALUES (1, 'US', 100.00);`
   - `CREATE SEMANTIC VIEW p67_quoted_fqn AS TABLES (o AS "stage 2"."my orders" PRIMARY KEY (id)) DIMENSIONS (o.region AS o.region) METRICS (o.total AS SUM(o.amount));`
   - Smoke + shape assertions pinning `FROM "memory"."stage 2"."my orders"`.

5. **Scenario 3 — Pathological: quoted name CONTAINING the literal "PRIMARY KEY"**:
   - `CREATE TABLE "weird PRIMARY KEY name" (id INTEGER PRIMARY KEY, region VARCHAR, amount DECIMAL(18,2));`
   - `INSERT INTO "weird PRIMARY KEY name" VALUES (1, 'US', 100.00);`
   - `CREATE SEMANTIC VIEW p67_pk_in_name AS TABLES (o AS "weird PRIMARY KEY name" PRIMARY KEY (id)) DIMENSIONS (o.region AS o.region) METRICS (o.total AS SUM(o.amount));`
   - Smoke check + shape assertion pinning `FROM "memory"."main"."weird PRIMARY KEY name"`.
   - This scenario proves the fix correctly disambiguates the inner-quoted `PRIMARY KEY` from the outer PK clause keyword. THIS is the canonical TECH-DEBT #24 case.

6. **Scenario 4 — Regression baseline (no whitespace) must still work**:
   - `CREATE TABLE p67_plain_orders (id INTEGER PRIMARY KEY, region VARCHAR, amount DECIMAL(18,2));`
   - `CREATE SEMANTIC VIEW p67_plain AS TABLES (o AS p67_plain_orders PRIMARY KEY (id)) DIMENSIONS (o.region AS o.region) METRICS (o.total AS SUM(o.amount));`
   - Smoke check passes — proves the fix did not regress the no-whitespace happy path.

7. **Cleanup**: `DROP SCHEMA IF EXISTS "stage 2" CASCADE;` at the end.

Authoring rules:
- Every `query I` block uses the `----\ntrue` separator form, never inline regex (CLAUDE.md).
- `statement ok` for CREATE/INSERT/DROP; no `statement error` blocks in this file (testing the SUCCESS case after the fix, not error wording).
- Append `phase67_quoted_source_tables.test` to `test/sql/TEST_LIST` (one line, no markers; CLAUDE.md mandatory).

If after Plan 01 Task 4 lands, `phase67_qualified_emission.test` is small enough (< 200 lines), the executor MAY choose to fold these 4 scenarios into that fixture instead of creating a separate file. In that case, skip the TEST_LIST registration (already done by Plan 01) and append the scenarios. Default per CONTEXT.md D-11: separate file.
  </action>
  <verify>
    <automated>just build > /tmp/claude/build_67_02_task3.log 2>&1; RC=$?; tail -40 /tmp/claude/build_67_02_task3.log; test "$RC" -eq 0 && just test-sql > /tmp/claude/test_sql_67_02_task3.log 2>&1; RC=$?; tail -120 /tmp/claude/test_sql_67_02_task3.log; test "$RC" -eq 0 && grep -c "^phase67_quoted_source_tables.test$" test/sql/TEST_LIST</automated>
  </verify>
  <acceptance_criteria>
    - `test/sql/phase67_quoted_source_tables.test` exists (or scenarios folded into `phase67_qualified_emission.test` with explicit SUMMARY note).
    - If standalone file: `grep -c "^phase67_quoted_source_tables.test$" test/sql/TEST_LIST` returns exactly 1.
    - `grep -c "my orders" test/sql/phase67_quoted_source_tables.test` returns >= 2 (internal-whitespace name used in CREATE TABLE + view ref).
    - `grep -c "weird PRIMARY KEY name" test/sql/phase67_quoted_source_tables.test` returns >= 1 (the canonical TECH-DEBT #24 case is exercised).
    - `just build` + `just test-sql` exit 0.
    - If `mktemp` is blocked, retry with `dangerouslyDisableSandbox: true` per CLAUDE.md Rule 2.
  </acceptance_criteria>
  <done>New fixture exercises 4 scenarios (quoted-whitespace, quoted-FQN, PK-in-name, regression baseline); TEST_LIST registered; `just test-sql` green.</done>
</task>

<task type="auto">
  <name>Task 4: Mark TECH-DEBT.md #24 RESOLVED + run full quality gate</name>
  <files>TECH-DEBT.md</files>
  <read_first>
    - TECH-DEBT.md (entry #24 in full; the surrounding entries to understand resolution-marking convention used by Phase 62 TECH-DEBT 20/22 resolutions — look for the pattern of marking resolved items vs. leaving them as `❓`)
    - The most recent Phase 62 commit referenced in MEMORY.md for resolution wording precedent ("TECH-DEBT 20 (bounded LRU eviction) and 22 (FALLBACK_OVERRIDE drops DISPLAY_EXTENSION_ERROR) marked resolved")
  </read_first>
  <action>
1. Open TECH-DEBT.md and locate entry #24 ("Body parser's TABLES clause splits on whitespace inside quoted source-table names").

2. Update the entry per the repo convention (look at how entries #20, #22 were marked resolved post-Phase-62 — likely a status flip from `❓` to `✅` and a resolution note appended at the end of the entry).

   Append a resolution block at the end of entry #24:
   ```
   - **Status:** ✅ RESOLVED in Phase 67 Plan 02 (commit <SHA-placeholder; fill in after commit lands>).
   - **Resolution:** `parse_single_table_entry` now consumes the source-table-name slot via `src/ident.rs::find_identifier_end`-driven dot-separated identifier advancement. Pathological case (`"weird PRIMARY KEY name"` as a source-table name) now parses correctly. Regression coverage: `test/sql/phase67_quoted_source_tables.test`.
   ```

   Flip any leading status emoji from `❓` to `✅` (or whatever the repo convention is — verify by reading how prior resolved entries are marked).

3. Do NOT delete the original entry body — preserve it for archival / change-history. Resolution is additive.

4. Run the full quality gate:
   - `just test-all > /tmp/claude/test_all_67_02_task4.log 2>&1` — Rust unit + proptest + sqllogictest + DuckLake CI.
   - `just ci > /tmp/claude/ci_67_02_task4.log 2>&1` — adds clippy pedantic + fmt + cargo-deny + fuzz target compile.
   - Both MUST exit 0.

5. If `mktemp` is blocked at any step, retry with `dangerouslyDisableSandbox: true` per CLAUDE.md Rule 2.

6. The commit SHA for the resolution annotation is filled in by the executor AFTER the commit lands. If this means a follow-up amend, that is acceptable (per CLAUDE.md: prefer NEW commits over amend, so use a follow-up commit `docs(67): backfill commit SHA in TECH-DEBT #24 resolution note` if convenient — OR leave the placeholder `<SHA>` and document in SUMMARY).
  </action>
  <verify>
    <automated>just test-all > /tmp/claude/test_all_67_02_task4.log 2>&1; RC=$?; tail -120 /tmp/claude/test_all_67_02_task4.log; echo "test-all RC=$RC"; test "$RC" -eq 0 && just ci > /tmp/claude/ci_67_02_task4.log 2>&1; RC=$?; tail -120 /tmp/claude/ci_67_02_task4.log; echo "ci RC=$RC"; test "$RC" -eq 0</automated>
  </verify>
  <acceptance_criteria>
    - `grep -n "Phase 67" TECH-DEBT.md` returns at least one match inside entry #24 (the resolution annotation).
    - The original entry #24 body is preserved (verifiable via `grep -c "Body parser.*splits on whitespace" TECH-DEBT.md` returning >= 1).
    - `just test-all` exits 0.
    - `just ci` exits 0.
    - If the executor adds a SHA placeholder rather than a real SHA, the SUMMARY notes it for a follow-up amend (or follow-up commit per the no-amend-prefer-new-commits rule).
  </acceptance_criteria>
  <done>TECH-DEBT #24 marked resolved with Phase 67 reference; full quality gate green.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| User-supplied DDL → body_parser | DDL text crosses from the parser_override C-API trampoline into `src/body_parser.rs::parse_single_table_entry`. Already-existing boundary; this plan tightens correctness on inputs that previously caused misparses, not new attack surface. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-67-02-01 | Tampering | `src/body_parser.rs::parse_single_table_entry` (user-supplied DDL parsed) | mitigate | Identifier-aware tokenisation via `find_identifier_end` (already-hardened helper from Phase 64). Rust safety bounds (no unsafe, no FFI introduced); panics on malformed input would be ParseError returns. No new untrusted-input surface beyond what `parse_single_table_entry` already accepts. |
| T-67-02-02 | Denial of Service | `find_identifier_end` loop in the new identifier-walk | accept | The loop advances `pos` by at least 1 byte per iteration (`find_identifier_end` returns >= 1 for non-empty input, plus the `pos += 1` dot-consumption step). Bounded by `after_as.len()`. No unbounded recursion or quadratic behaviour. |

No new high-severity threats. The fix replaces a known-buggy whitespace tokeniser with a known-hardened identifier-aware one (Phase 64 `find_identifier_end` has 50+ unit tests and ships in production at every OTHER capture site since Phase 64). Reuse-of-hardened-helper REDUCES total attack surface vs. introducing a new parser.
</threat_model>

<verification>
- `cargo test -p semantic-views --lib` exits 0 with the 4+ new unit tests added in Task 1.
- `cargo clippy --all-targets -- -D warnings` exits 0.
- `cargo fmt --check` exits 0 (pre-commit hook enforces this).
- The audit-grep result from Task 2 is recorded in SUMMARY.md with each site classified.
- `test/sql/phase67_quoted_source_tables.test` (or folded equivalent in `phase67_qualified_emission.test`) is registered in TEST_LIST.
- `just test-sql` exits 0 with the new fixture green.
- `just test-all` exits 0 — Rust unit + proptest + sqllogictest + DuckLake CI.
- `just ci` exits 0 — adds clippy pedantic + fmt + cargo-deny + fuzz target compile.
- TECH-DEBT.md #24 has a resolution annotation citing Phase 67 Plan 02.
- Tail-buffer rule honored: all build/test commands redirect to `/tmp/claude/*.log` first, then `tail -N`. No bare `tail` piping (CLAUDE.md Rule 1).
- Sandbox bypass (`dangerouslyDisableSandbox: true`) used only when `mktemp: Operation not permitted` surfaces, and only for build/test commands per CLAUDE.md Rule 2.
</verification>

<success_criteria>
- `src/body_parser.rs::parse_single_table_entry` consumes the source-table-name slot via `find_identifier_end` (verifiable via grep + AST inspection).
- At least 4 new Rust unit tests prove the fix on the cases enumerated in `<behavior>`.
- Audit grep over `src/body_parser.rs` ran; every `split_whitespace` site is classified and disposed of in the SUMMARY.
- `test/sql/phase67_quoted_source_tables.test` (or folded equivalent) exists; registered in TEST_LIST.
- TECH-DEBT.md entry #24 marked RESOLVED with Phase 67 reference.
- `just test-all` and `just ci` both exit 0.
- No changes to `src/ident.rs` (D-09).
- No release-prep tasks (CHANGELOG, Cargo.toml, etc.) included.
</success_criteria>

<output>
Create `.planning/phases/67-expansion-sql-coverage-and-tech-debt-cleanup/67-02-SUMMARY.md` when done. Include:
- The exact line numbers / byte offsets of the surgery in `src/body_parser.rs::parse_single_table_entry`
- The audit-grep output from Task 2 (every `split_whitespace` site, classification, disposition)
- The 4 (or more) scenarios in the new fixture and which TECH-DEBT #24 cases each one exercises
- The TECH-DEBT.md #24 resolution annotation as committed (with SHA placeholder if not yet backfilled)
- Confirmation that `just test-all` and `just ci` both exit 0
</output>
