---
phase: 34.1.1-close-gaps-with-snowflake-show-semantic-ddls-add-like-starts-with-limit
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/parse.rs
autonomous: true
requirements:
  - DDL-08
  - DDL-09
  - DDL-10
  - DDL-11
  - DDL-12
  - DDL-13
  - DDL-14
  - DDL-15
  - DDL-16
  - DDL-17
  - DDL-18
  - DDL-19

must_haves:
  truths:
    - "SHOW SEMANTIC DIMENSIONS LIKE '%rev%' produces SQL with WHERE name ILIKE '%rev%'"
    - "SHOW SEMANTIC METRICS STARTS WITH 'total' produces SQL with WHERE name LIKE 'total%'"
    - "SHOW SEMANTIC FACTS LIMIT 5 produces SQL with LIMIT 5"
    - "All three clauses compose: LIKE + STARTS WITH + LIMIT in one statement"
    - "Clause order enforced: LIKE before IN, STARTS WITH after IN/FOR METRIC, LIMIT last"
    - "FOR METRIC variant supports LIKE, STARTS WITH, LIMIT"
    - "Both IN (single-view) and cross-view forms support all filter clauses"
    - "Single quotes in patterns are properly escaped (SQL injection safe)"
  artifacts:
    - path: "src/parse.rs"
      provides: "LIKE/STARTS WITH/LIMIT clause parsing and SQL rewrite for SHOW commands"
      contains: "fn extract_quoted_string"
    - path: "src/parse.rs"
      provides: "Filter suffix builder"
      contains: "fn build_filter_suffix"
  key_links:
    - from: "rewrite_ddl (SHOW arm)"
      to: "extract_quoted_string + build_filter_suffix"
      via: "clause parsing calls helpers, appends suffix to generated SELECT"
      pattern: "build_filter_suffix"
    - from: "rewrite_show_dims_for_metric"
      to: "extract_quoted_string + build_filter_suffix"
      via: "FOR METRIC path also collects and appends LIKE/STARTS WITH/LIMIT"
      pattern: "build_filter_suffix"
---

<objective>
Add LIKE, STARTS WITH, and LIMIT clause parsing to all four SHOW SEMANTIC command forms in the parser rewrite layer, with comprehensive Rust unit tests.

Purpose: Achieve Snowflake feature parity for SHOW SEMANTIC DIMENSIONS/METRICS/FACTS filtering. All filtering is handled by appending WHERE/LIMIT clauses to the generated SELECT statements -- no VTab changes needed.

Output: Modified `src/parse.rs` with new helper functions, updated rewrite logic for all SHOW paths, and Rust unit tests covering every clause combination.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/34.1.1-close-gaps-with-snowflake-show-semantic-ddls-add-like-starts-with-limit/34.1.1-RESEARCH.md

<interfaces>
<!-- Key types and functions the executor needs from src/parse.rs -->

From src/parse.rs (current SHOW arm, lines 296-351):
```rust
// rewrite_ddl match arm for SHOW DIMENSIONS/METRICS/FACTS:
DdlKind::ShowDimensions | DdlKind::ShowMetrics | DdlKind::ShowFacts => {
    let after_prefix = trimmed[plen..].trim();
    if after_prefix.is_empty() {
        // Cross-view: SELECT * FROM show_semantic_dimensions_all()
    } else {
        // Check for IN keyword, extract view_name
        // If rest after view_name: FOR METRIC dispatch (ShowDimensions only)
    }
}
```

From src/parse.rs (rewrite_show_dims_for_metric, lines 230-264):
```rust
fn rewrite_show_dims_for_metric(view_name: &str, rest: &str) -> Result<String, String> {
    // Parses "FOR METRIC metric_name" from rest
    // Returns: SELECT * FROM show_semantic_dimensions_for_metric('view', 'metric')
}
```

From src/parse.rs (extract_ddl_name SHOW arm, lines 419-439):
```rust
DdlKind::ShowDimensions | DdlKind::ShowMetrics | DdlKind::ShowFacts => {
    // Currently: checks for IN keyword directly after prefix
    // Needs updating: LIKE can appear before IN
}
```

Cross-view function names:
- show_semantic_dimensions_all()
- show_semantic_metrics_all()
- show_semantic_facts_all()

Single-view function names:
- show_semantic_dimensions('view')
- show_semantic_metrics('view')
- show_semantic_facts('view')

FOR METRIC function:
- show_semantic_dimensions_for_metric('view', 'metric')
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add helper functions and extend SHOW rewrite with LIKE/STARTS WITH/LIMIT</name>
  <files>src/parse.rs</files>
  <read_first>
    - src/parse.rs (entire file -- the executor modifies this file)
    - .planning/phases/34.1.1-close-gaps-with-snowflake-show-semantic-ddls-add-like-starts-with-limit/34.1.1-RESEARCH.md (code examples in the research)
  </read_first>

  <behavior>
    - Test: `extract_quoted_string("'hello'")` returns `Ok(("hello", 7))`
    - Test: `extract_quoted_string("'O''Brien'")` returns `Ok(("O'Brien", 10))`
    - Test: `extract_quoted_string("''")` returns `Ok(("", 2))` (empty string)
    - Test: `extract_quoted_string("'unterminated")` returns `Err`
    - Test: `extract_quoted_string("no_quote")` returns `Err`
    - Test: `build_filter_suffix(Some("%rev%"), None, None)` returns `" WHERE name ILIKE '%rev%'"`
    - Test: `build_filter_suffix(None, Some("total"), None)` returns `" WHERE name LIKE 'total%'"`
    - Test: `build_filter_suffix(None, None, Some(5))` returns `" LIMIT 5"`
    - Test: `build_filter_suffix(Some("%x%"), Some("a"), Some(10))` returns `" WHERE name ILIKE '%x%' AND name LIKE 'a%' LIMIT 10"`
    - Test: `build_filter_suffix(None, None, None)` returns `""`
    - Test: `build_filter_suffix(Some("O''Brien"), None, None)` returns `" WHERE name ILIKE 'O''''Brien'"` (re-escapes quotes)
    - Test: `rewrite_ddl("SHOW SEMANTIC DIMENSIONS LIKE '%rev%'")` returns `Ok("SELECT * FROM show_semantic_dimensions_all() WHERE name ILIKE '%rev%'")`
    - Test: `rewrite_ddl("SHOW SEMANTIC DIMENSIONS LIKE '%c%' IN v STARTS WITH 'cust' LIMIT 2")` returns `Ok("SELECT * FROM show_semantic_dimensions('v') WHERE name ILIKE '%c%' AND name LIKE 'cust%' LIMIT 2")`
    - Test: `rewrite_ddl("SHOW SEMANTIC METRICS STARTS WITH 'total' LIMIT 1")` returns `Ok("SELECT * FROM show_semantic_metrics_all() WHERE name LIKE 'total%' LIMIT 1")`
    - Test: `rewrite_ddl("SHOW SEMANTIC FACTS LIMIT 10")` returns `Ok("SELECT * FROM show_semantic_facts_all() LIMIT 10")`
    - Test: `rewrite_ddl("SHOW SEMANTIC DIMENSIONS LIKE '%x%' IN v FOR METRIC m STARTS WITH 'a' LIMIT 3")` returns `Ok("SELECT * FROM show_semantic_dimensions_for_metric('v', 'm') WHERE name ILIKE '%x%' AND name LIKE 'a%' LIMIT 3")`
    - Test: `rewrite_ddl("SHOW SEMANTIC DIMENSIONS IN v LIKE '%x%'")` returns `Err` (LIKE must appear before IN)
    - Test: `rewrite_ddl("SHOW SEMANTIC METRICS LIMIT abc")` returns `Err` (non-numeric LIMIT)
    - Test: `extract_ddl_name("SHOW SEMANTIC DIMENSIONS LIKE '%x%' IN v")` returns `Ok(Some("v"))` (LIKE before IN still extracts view name)
    - Test: `extract_ddl_name("SHOW SEMANTIC DIMENSIONS LIKE '%x%'")` returns `Ok(None)` (cross-view with LIKE)
  </behavior>

  <action>
    Add two new helper functions to `src/parse.rs` above the `rewrite_ddl` function (around line 266):

    **1. `extract_quoted_string(input: &str) -> Result<(String, usize), String>`**

    Extract a single-quoted string from `input`, starting at position 0. Returns `(extracted_content, bytes_consumed)` where bytes_consumed includes opening and closing quotes. Handles SQL-style `''` escape (doubled single quote = literal single quote). Returns error on unterminated string or missing opening quote.

    Implementation (from research):
    ```rust
    fn extract_quoted_string(input: &str) -> Result<(String, usize), String> {
        let bytes = input.as_bytes();
        if bytes.is_empty() || bytes[0] != b'\'' {
            return Err("Expected single-quoted string".to_string());
        }
        let mut pos = 1;
        let mut result = String::new();
        while pos < bytes.len() {
            if bytes[pos] == b'\'' {
                if pos + 1 < bytes.len() && bytes[pos + 1] == b'\'' {
                    result.push('\'');
                    pos += 2;
                } else {
                    return Ok((result, pos + 1));
                }
            } else {
                result.push(bytes[pos] as char);
                pos += 1;
            }
        }
        Err("Unterminated single-quoted string".to_string())
    }
    ```

    **2. `build_filter_suffix(like_pattern: Option<&str>, starts_with: Option<&str>, limit: Option<u64>) -> String`**

    Build optional WHERE and LIMIT suffix. LIKE maps to `name ILIKE '<escaped>'` (case-insensitive). STARTS WITH maps to `name LIKE '<escaped>%'` (case-sensitive). Both conditions combined with AND. LIMIT appended last.

    Implementation (from research):
    ```rust
    fn build_filter_suffix(
        like_pattern: Option<&str>,
        starts_with: Option<&str>,
        limit: Option<u64>,
    ) -> String {
        let mut parts = Vec::new();
        if let Some(pattern) = like_pattern {
            let escaped = pattern.replace('\'', "''");
            parts.push(format!("name ILIKE '{escaped}'"));
        }
        if let Some(prefix) = starts_with {
            let escaped = prefix.replace('\'', "''");
            parts.push(format!("name LIKE '{escaped}%'"));
        }
        let mut suffix = String::new();
        if !parts.is_empty() {
            suffix.push_str(" WHERE ");
            suffix.push_str(&parts.join(" AND "));
        }
        if let Some(n) = limit {
            suffix.push_str(&format!(" LIMIT {n}"));
        }
        suffix
    }
    ```

    **3. Add `parse_show_filter_clauses` helper function**

    This function takes the `after_prefix` text (everything after `SHOW SEMANTIC DIMENSIONS/METRICS/FACTS`) and parses all optional clauses in Snowflake order: LIKE, IN, FOR METRIC, STARTS WITH, LIMIT.

    ```rust
    /// Parsed filter clauses from a SHOW SEMANTIC command.
    struct ShowClauses<'a> {
        like_pattern: Option<String>,
        in_view: Option<&'a str>,
        for_metric: Option<&'a str>,  // only valid for ShowDimensions
        starts_with: Option<String>,
        limit: Option<u64>,
    }
    ```

    Parse logic:
    - Start with `after_prefix` trimmed
    - Check for `LIKE` keyword (case-insensitive): if found, consume `LIKE`, skip whitespace, call `extract_quoted_string` to get the pattern, advance position
    - Check for `IN` keyword (case-insensitive, must be followed by whitespace): if found, consume `IN`, skip whitespace, extract view_name (up to next whitespace or end)
    - Check for `FOR METRIC` keywords (case-insensitive, only for ShowDimensions): if found, consume `FOR`, skip whitespace, consume `METRIC`, skip whitespace, extract metric_name (up to next whitespace or end)
    - Check for `STARTS WITH` keywords (case-insensitive): if found, consume `STARTS`, skip whitespace, consume `WITH`, skip whitespace, call `extract_quoted_string` to get the prefix
    - Check for `LIMIT` keyword (case-insensitive): if found, consume `LIMIT`, skip whitespace, parse the next token as `u64`. If not a valid positive integer, return error: `"LIMIT must be a positive integer, got: '<token>'"`
    - If any text remains after all clauses, return error with usage hint

    **4. Modify `rewrite_ddl` SHOW arm (lines 296-351)**

    Replace the existing `DdlKind::ShowDimensions | DdlKind::ShowMetrics | DdlKind::ShowFacts` match arm to:
    1. Call `parse_show_filter_clauses` on `after_prefix`
    2. If `for_metric` is `Some` but `kind != ShowDimensions`, return error: `"FOR METRIC is only valid for SHOW SEMANTIC DIMENSIONS"`
    3. Build the base SELECT:
       - If `in_view` is None and `for_metric` is None: use the `_all()` function
       - If `in_view` is Some and `for_metric` is None: use the single-view function with `'view_name'`
       - If `in_view` is Some and `for_metric` is Some: use `show_semantic_dimensions_for_metric('view', 'metric')`
       - If `in_view` is None and `for_metric` is Some: return error (`FOR METRIC requires IN view_name`)
    4. Append `build_filter_suffix(like_pattern, starts_with, limit)` to the base SELECT
    5. Return the composed SQL

    The `rewrite_show_dims_for_metric` function (line 230) is now DEAD CODE -- remove it entirely. Its logic is absorbed into the new clause parser.

    **5. Modify `extract_ddl_name` SHOW arm (lines 419-439)**

    The `extract_ddl_name` function for SHOW DIMS/METRICS/FACTS must handle LIKE appearing before IN. Update:
    - After getting `after_prefix`, check if it starts with `LIKE` (case-insensitive). If so, consume LIKE + quoted string to advance past it.
    - Then check for `IN` keyword as before.
    - This ensures `extract_ddl_name("SHOW SEMANTIC DIMENSIONS LIKE '%x%' IN v")` correctly returns `Some("v")`.

    **6. Update error messages in usage hints to include new clauses**

    The error message when unexpected tokens appear after the entity name should now show the full syntax:
    ```
    Usage: SHOW SEMANTIC DIMENSIONS [LIKE '<pattern>'] [IN view_name] [STARTS WITH '<prefix>'] [LIMIT <n>]
    ```
    And for dimensions with FOR METRIC:
    ```
    Usage: SHOW SEMANTIC DIMENSIONS [LIKE '<pattern>'] [IN view_name] [FOR METRIC metric_name] [STARTS WITH '<prefix>'] [LIMIT <n>]
    ```

    **7. Add Rust unit tests in the `#[cfg(test)]` module**

    Add a new `mod phase34_1_1_show_filter_tests` test module at the end of the test section (before the final closing `}`) with tests for:
    - `extract_quoted_string`: normal, escaped quotes, empty string, unterminated, no opening quote
    - `build_filter_suffix`: each combination of None/Some for the three parameters
    - `rewrite_ddl` SHOW DIMENSIONS with LIKE only (cross-view)
    - `rewrite_ddl` SHOW DIMENSIONS with LIKE + IN (single-view)
    - `rewrite_ddl` SHOW DIMENSIONS with LIKE + IN + STARTS WITH + LIMIT (full composition)
    - `rewrite_ddl` SHOW METRICS with STARTS WITH only
    - `rewrite_ddl` SHOW FACTS with LIMIT only
    - `rewrite_ddl` SHOW DIMENSIONS with LIKE + IN + FOR METRIC + STARTS WITH + LIMIT (full FOR METRIC path)
    - `rewrite_ddl` error: LIKE after IN (wrong clause order)
    - `rewrite_ddl` error: non-numeric LIMIT
    - `rewrite_ddl` error: FOR METRIC on SHOW METRICS (invalid)
    - `extract_ddl_name` with LIKE before IN correctly extracts view name
    - `extract_ddl_name` with LIKE only (cross-view) returns None
    - Case insensitivity: `show semantic dimensions like '%x%' in v` (all lowercase)
  </action>

  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test -- parse 2>&1 | tail -20</automated>
  </verify>

  <acceptance_criteria>
    - src/parse.rs contains `fn extract_quoted_string(input: &str) -> Result<(String, usize), String>`
    - src/parse.rs contains `fn build_filter_suffix(`
    - src/parse.rs contains `struct ShowClauses`
    - src/parse.rs contains `ILIKE` (for LIKE clause mapping)
    - src/parse.rs contains `mod phase34_1_1_show_filter_tests`
    - src/parse.rs does NOT contain the old standalone `fn rewrite_show_dims_for_metric` function (absorbed into clause parser)
    - `cargo test -- parse` exits 0
    - `cargo test -- phase34_1_1` exits 0
    - `cargo test -- extract_quoted_string` exits 0
    - `cargo test -- build_filter_suffix` exits 0
  </acceptance_criteria>

  <done>
    All SHOW SEMANTIC DIMENSIONS/METRICS/FACTS parser rewrite paths handle LIKE, STARTS WITH, and LIMIT clauses. Clause order matches Snowflake (LIKE before IN, STARTS WITH after IN/FOR METRIC, LIMIT last). Single quotes in patterns are escaped. FOR METRIC variant includes all filter clauses. All Rust unit tests pass.
  </done>
</task>

<task type="auto">
  <name>Task 2: Integration tests via sqllogictest</name>
  <files>test/sql/phase34_1_1_show_filtering.test</files>
  <read_first>
    - test/sql/phase34_1_show_commands.test (existing SHOW test patterns and setup)
    - test/sql/phase34_1_show_dims_for_metric.test (existing FOR METRIC test patterns)
    - src/parse.rs (the updated code from Task 1)
  </read_first>

  <action>
    Create `test/sql/phase34_1_1_show_filtering.test` -- a new sqllogictest file that exercises LIKE, STARTS WITH, and LIMIT through the full extension pipeline (load extension, create tables, create semantic views, run SHOW commands with filter clauses, verify output).

    Use table prefix `p3411_` to avoid collisions with other test files.

    **Setup section:**

    ```sql
    CREATE TABLE p3411_customers (id INTEGER PRIMARY KEY, name VARCHAR, region VARCHAR);
    CREATE TABLE p3411_orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount DECIMAL(10,2), order_date DATE);

    CREATE SEMANTIC VIEW filter_sv AS
      TABLES (
        c AS p3411_customers PRIMARY KEY (id),
        o AS p3411_orders PRIMARY KEY (id)
      )
      RELATIONSHIPS (
        orders_to_customers AS o(customer_id) REFERENCES c
      )
      FACTS (
        o.raw_amount AS o.amount
      )
      DIMENSIONS (
        c.customer_name AS c.name,
        c.region AS c.region,
        o.order_date AS o.order_date
      )
      METRICS (
        o.total_amount AS SUM(o.amount),
        o.order_count AS COUNT(o.id)
      )
    ```

    Also create a second view for cross-view tests:
    ```sql
    CREATE TABLE p3411_products (id INTEGER PRIMARY KEY, product_name VARCHAR, price DECIMAL(10,2));

    CREATE SEMANTIC VIEW products_sv AS
      TABLES (
        p AS p3411_products PRIMARY KEY (id)
      )
      DIMENSIONS (
        p.product_name AS p.product_name
      )
      METRICS (
        p.avg_price AS AVG(p.price)
      )
    ```

    **Test cases (each as a separate numbered section):**

    1. **SHOW SEMANTIC DIMENSIONS LIKE '%name%'** (cross-view): should return `customer_name` from filter_sv and `product_name` from products_sv (both match `%name%`). Use `query TTTTT rowsort`.

    2. **SHOW SEMANTIC DIMENSIONS LIKE '%order%'** (cross-view): should return only `order_date` from filter_sv (matches `%order%`). Use `query TTTTT rowsort`.

    3. **SHOW SEMANTIC DIMENSIONS LIKE '%NAME%' IN filter_sv** (single-view, case-insensitive): should return `customer_name` only (`%NAME%` matches case-insensitively). Use `query TTTTT rowsort`.

    4. **SHOW SEMANTIC DIMENSIONS STARTS WITH 'customer'** (cross-view, case-sensitive): should return `customer_name` from filter_sv only. Use `query TTTTT rowsort`.

    5. **SHOW SEMANTIC DIMENSIONS STARTS WITH 'Customer'** (cross-view, case-sensitive, wrong case): should return 0 rows (case-sensitive, no dim starts with uppercase 'C'). Use `query TTTTT rowsort` with empty result.

    6. **SHOW SEMANTIC DIMENSIONS LIMIT 2** (cross-view): should return at most 2 rows. Use `query TTTTT` (without rowsort since LIMIT order is nondeterministic -- just check we get exactly 2 rows). Actually, use `query TTTTT rowsort` and expect exactly 2 rows from the 4 total dimensions. Since rowsort is deterministic, the first 2 alphabetically are: `customer_name` and `order_date` (from filter_sv). BUT LIMIT is applied before rowsort in the query engine, so result is nondeterministic. Instead, just use `statement ok` to verify it does not error, then use a `query I` with `SELECT COUNT(*) FROM (SHOW SEMANTIC DIMENSIONS LIMIT 2)` to check exactly 2 rows. WAIT -- sqllogictest might not support subquery of DDL. Instead, verify via `query TTTTT` (no rowsort) and use `halt` on row count. Simplest: use `query TTTTT` and accept that order is nondeterministic. Just test that it does not error. For deterministic testing, use LIMIT with LIKE to make the result set known:

    6. **SHOW SEMANTIC DIMENSIONS LIKE '%name%' LIMIT 1** (cross-view): `%name%` matches `customer_name` and `product_name` (2 matches), LIMIT 1 returns exactly 1 row. Use `query TTTTT` (single row, no rowsort needed). Since order is nondeterministic, just verify it runs without error and returns 1 row.

    Actually the simplest approach: use `SHOW SEMANTIC DIMENSIONS IN filter_sv LIMIT 2` which has 3 dims, limited to 2. Result is deterministic from the VTab (it emits in declaration order: customer_name, region, order_date). LIMIT 2 returns customer_name and region. Use `query TTTTT rowsort`.

    Let me reconsider. The DuckDB LIMIT on a VTab scan is implementation-defined in row order. Safest:
    - Test LIMIT by verifying the count, not exact rows
    - Use `SHOW SEMANTIC DIMENSIONS IN filter_sv LIMIT 2` and just check no error + exactly 2 rows

    For sqllogictest, we can test LIMIT determinism by combining with a WHERE clause. But simplest is just testing it works.

    Revised test plan:

    6. **SHOW SEMANTIC DIMENSIONS IN filter_sv LIMIT 2**: runs without error. Verify by wrapping: use `query TTTTT` and expect exactly 2 rows.

    7. **SHOW SEMANTIC METRICS LIKE '%amount%'** (cross-view): returns only `total_amount` from filter_sv. Use `query TTTTT rowsort`.

    8. **SHOW SEMANTIC METRICS STARTS WITH 'order'** (cross-view): returns only `order_count` from filter_sv. Use `query TTTTT rowsort`.

    9. **SHOW SEMANTIC FACTS LIKE '%amount%'** (cross-view): returns `raw_amount` from filter_sv. Use `query TTTT rowsort` (facts have 4 columns, no data_type).

    10. **SHOW SEMANTIC FACTS STARTS WITH 'raw'** (cross-view): returns `raw_amount`. Use `query TTTT rowsort`.

    11. **SHOW SEMANTIC FACTS IN filter_sv LIMIT 1**: returns exactly 1 fact row. Use `query TTTT`.

    12. **Composition: SHOW SEMANTIC DIMENSIONS LIKE '%name%' IN filter_sv STARTS WITH 'cust' LIMIT 10**: matches `customer_name` (passes both LIKE '%name%' and STARTS WITH 'cust'). Should return 1 row. Use `query TTTTT rowsort`.

    13. **FOR METRIC with LIKE: SHOW SEMANTIC DIMENSIONS LIKE '%r%' IN filter_sv FOR METRIC total_amount**: fan-trap-safe dims for `total_amount` are `customer_name`, `region` (both from `c` table -- safe ManyToOne). Of those, `%r%` matches only `region`. Use `query TTTTT rowsort`.

    14. **FOR METRIC with STARTS WITH: SHOW SEMANTIC DIMENSIONS IN filter_sv FOR METRIC total_amount STARTS WITH 'customer'**: returns `customer_name` only. Use `query TTTTT rowsort`.

    15. **FOR METRIC with LIMIT: SHOW SEMANTIC DIMENSIONS IN filter_sv FOR METRIC total_amount LIMIT 1**: returns 1 of the 2 safe dims. Use `query TTTTT`.

    16. **Error: wrong clause order**: `SHOW SEMANTIC DIMENSIONS IN filter_sv LIKE '%x%'` should produce a `statement error`.

    17. **Case insensitivity of keywords**: `show semantic dimensions like '%name%' in filter_sv` (all lowercase) should work. Use `query TTTTT rowsort`.

    18. **Empty LIKE pattern**: `SHOW SEMANTIC DIMENSIONS LIKE '' IN filter_sv` should return 0 rows (empty pattern matches nothing with ILIKE). Actually, `'' ILIKE ''` in DuckDB returns true. Empty LIKE `''` matches only empty strings. Since no dim name is empty, this should return 0 rows. Use `query TTTTT rowsort` with no result rows.

    Actually, `ILIKE ''` matches only empty strings. No dimension name is empty, so 0 rows. But safer to just test it does not error: use `statement ok` if we cannot guarantee behavior. Actually, `SELECT 'hello' ILIKE ''` returns false in DuckDB, so `LIKE ''` will return 0 rows. Test with `query TTTTT rowsort` and expect no output rows.

    **Cleanup section:**
    ```sql
    DROP SEMANTIC VIEW filter_sv;
    DROP SEMANTIC VIEW products_sv;
    DROP TABLE p3411_orders;
    DROP TABLE p3411_customers;
    DROP TABLE p3411_products;
    ```

    **Important notes for the executor:**
    - The test file MUST start with `require semantic_views`
    - Use tab-separated expected output (matching existing test file conventions)
    - Dimensions/metrics have 5 output columns (TTTTT): semantic_view_name, name, expr, source_table, data_type
    - Facts have 4 output columns (TTTT): semantic_view_name, name, expr, source_table (no data_type)
    - The `data_type` column for dims/metrics currently shows `(empty)` since types are not resolved at SHOW time
    - `statement error` tests should include a substring of the expected error message after `----`
    - Run `just build` before `just test-sql` since sqllogictest requires the compiled extension
  </action>

  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && just build && just test-sql 2>&1 | tail -20</automated>
  </verify>

  <acceptance_criteria>
    - test/sql/phase34_1_1_show_filtering.test exists
    - test/sql/phase34_1_1_show_filtering.test contains `require semantic_views`
    - test/sql/phase34_1_1_show_filtering.test contains `LIKE '%name%'`
    - test/sql/phase34_1_1_show_filtering.test contains `STARTS WITH 'customer'`
    - test/sql/phase34_1_1_show_filtering.test contains `LIMIT`
    - test/sql/phase34_1_1_show_filtering.test contains `FOR METRIC`
    - test/sql/phase34_1_1_show_filtering.test contains at least 15 test sections (numbered comments)
    - test/sql/phase34_1_1_show_filtering.test contains `statement error` (at least 1 negative test)
    - `just build && just test-sql` exits 0
    - `just test-all` exits 0
  </acceptance_criteria>

  <done>
    All SHOW SEMANTIC filtering clauses (LIKE, STARTS WITH, LIMIT) are verified end-to-end through the full extension pipeline: parser rewrite generates correct SQL, DuckDB executes it against VTab output, correct filtered rows are returned. Clause composition, FOR METRIC filtering, cross-view forms, error cases, and case insensitivity are all covered. `just test-all` passes.
  </done>
</task>

</tasks>

<verification>
After both tasks complete:
1. `cargo test` -- all Rust unit tests pass (including new phase34_1_1 tests)
2. `just build && just test-sql` -- all sqllogictest files pass
3. `just test-all` -- full quality gate passes
</verification>

<success_criteria>
1. All four SHOW SEMANTIC command forms (DIMENSIONS, METRICS, FACTS, DIMENSIONS FOR METRIC) support LIKE, STARTS WITH, and LIMIT clauses
2. Clause order matches Snowflake: LIKE before IN, STARTS WITH after IN/FOR METRIC, LIMIT last
3. LIKE maps to DuckDB ILIKE (case-insensitive), STARTS WITH maps to DuckDB LIKE with % suffix (case-sensitive)
4. All three clauses compose (can be used together in one statement)
5. Single quotes in patterns are properly escaped
6. Both IN (single-view) and cross-view forms support all filter clauses
7. Wrong clause order produces clear error messages
8. `just test-all` passes
</success_criteria>

<output>
After completion, create `.planning/phases/34.1.1-close-gaps-with-snowflake-show-semantic-ddls-add-like-starts-with-limit/34.1.1-01-SUMMARY.md`
</output>
