---
phase: 14-ducklake-integration-test-refresh-and-ci-job
plan: "02"
type: execute
wave: 1
depends_on: []
files_modified:
  - test/integration/test_ducklake.py
autonomous: true
requirements: [DUCKLAKE-LOCAL]

must_haves:
  truths:
    - "test_ducklake.py uses only v0.2.0 API — no calls to define_semantic_view or semantic_query"
    - "test_ducklake.py has 6 test cases (4 original + typed BIGINT assertion + time dimension test)"
    - "test_ducklake.py imports shared helpers from test_ducklake_helpers instead of duplicating boilerplate"
    - "Typed output assertion: order_count returns Python int, not str"
    - "Time dimension test declares ordered_at as time-typed with day granularity"
  artifacts:
    - path: "test/integration/test_ducklake.py"
      provides: "Updated local DuckLake integration test (v0.2.0 API)"
  key_links:
    - from: "test_ducklake.py"
      to: "create_semantic_view (v0.2.0 API)"
      via: "STRUCT/LIST arguments"
      pattern: "create_semantic_view"
    - from: "test_ducklake.py"
      to: "semantic_view (v0.2.0 API)"
      via: "table function"
      pattern: "semantic_view"
---

<objective>
Update test/integration/test_ducklake.py from v0.1.0 API to v0.2.0 API and add 2 new test cases.

Purpose: The existing local test calls `define_semantic_view` (removed) and `semantic_query` (renamed). It will fail against the current build. This plan updates it to be correct against the v0.2.0 codebase.
Output: Updated test_ducklake.py with v0.2.0 API, 6 test cases total, shared helpers import.
</objective>

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

<context>
@.planning/phases/14-ducklake-integration-test-refresh-and-ci-job/14-CONTEXT.md
@.planning/phases/14-ducklake-integration-test-refresh-and-ci-job/14-RESEARCH.md
@test/integration/test_ducklake.py

<interfaces>
<!-- v0.2.0 API confirmed in src/lib.rs and test/sql/phase4_query.test -->

v0.1.0 → v0.2.0 migration:
  define_semantic_view('name', json_string)  →  create_semantic_view(name, tables, rels, dims, time_dims, metrics)
  semantic_query('name', ...)                →  semantic_view('name', ...)
  drop_semantic_view('name')                 →  unchanged
  explain_semantic_view(...)                 →  unchanged

create_semantic_view 6-arg STRUCT/LIST signature:
  arg 0: name VARCHAR
  arg 1: tables LIST(STRUCT(alias VARCHAR, "table" VARCHAR))
  arg 2: relationships LIST(STRUCT(...))  — empty list [] for single-table views
  arg 3: dimensions LIST(STRUCT(name VARCHAR, expr VARCHAR, source_table VARCHAR))
  arg 4: time_dimensions LIST(STRUCT(name VARCHAR, expr VARCHAR, granularity VARCHAR))
  arg 5: metrics LIST(STRUCT(name VARCHAR, expr VARCHAR, source_table VARCHAR))

jaffle-shop raw_orders actual columns:
  id, customer, ordered_at, store_id, subtotal, tax_paid, order_total

Typed output (Phase 12/13):
  count(*) returns BIGINT → Python int
  sum(order_total) returns BIGINT → Python int (order_total is INTEGER)
  DATE columns return datetime.date
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Rewrite test_ducklake.py — update to v0.2.0 API and add 2 new test cases</name>
  <files>test/integration/test_ducklake.py</files>
  <action>
Rewrite `test/integration/test_ducklake.py` keeping the existing structure (PEP 723 header, docstring, check_prerequisites, run_tests, main pattern) but:

**1. Keep PEP 723 header:**
```python
# /// script
# dependencies = ["duckdb"]
# requires-python = ">=3.9"
# ///
```

**2. Update imports** — add helpers import after the path constants:
```python
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
from test_ducklake_helpers import get_ext_dir, load_extension, attach_ducklake
```

**3. Update `check_prerequisites()`** — remove the check for CATALOG_DB and DUCKLAKE_FILE (those are still required for the local test, but don't check for them in the function — the local test uses the same DATA_DIR/CATALOG_DB/DUCKLAKE_FILE constants that already exist). Keep the EXTENSION_PATH check. Keep the instructions to run `just setup-ducklake`.

Actually: keep the existing check_prerequisites() structure intact — it already checks CATALOG_DB, DUCKLAKE_FILE, EXTENSION_PATH. The local test still requires jaffle-shop data. Keep this unchanged.

**4. Update `run_tests()`:**

Replace the `ext_dir` + connection + INSTALL/LOAD block at the top of run_tests() with:
```python
ext_dir = get_ext_dir()
con = duckdb.connect(str(CATALOG_DB), config={
    "allow_unsigned_extensions": "true",
    "extension_directory": ext_dir,
})
load_extension(con, EXTENSION_PATH)
attach_ducklake(con, str(DUCKLAKE_FILE), str(JAFFLE_DATA_DIR) + "/", alias='jaffle')
```

Remove the manual `ATTACH` call (now in helper).

**5. Update Test 1 — Define semantic view:**
Replace the old `define_semantic_view` call with v0.2.0 `create_semantic_view`:
```python
con.execute("""
    SELECT create_semantic_view(
        'jaffle_orders',
        [{'alias': 'o', 'table': 'jaffle.raw_orders'}],
        [],
        [{'name': 'store_id', 'expr': 'store_id', 'source_table': 'o'}],
        [{'name': 'ordered_at', 'expr': 'ordered_at', 'granularity': 'day'}],
        [{'name': 'order_count', 'expr': 'count(*)', 'source_table': 'o'},
         {'name': 'total_revenue', 'expr': 'sum(order_total)', 'source_table': 'o'}]
    )
""")
```
Note: we include time_dimension in the initial definition (arg 4) so it's available for Test 6.

**6. Update Test 2 — Query with dimension:**
Replace `semantic_query` with `semantic_view`:
```python
result = con.execute("""
    SELECT * FROM semantic_view(
        'jaffle_orders',
        dimensions := ['store_id'],
        metrics := ['order_count']
    )
""").fetchall()
```
Keep the assertions (len > 0, distinct store_ids > 0). Update assertion message if needed.

**7. Update Test 3 — Global aggregate:**
Replace `semantic_query` with `semantic_view`:
```python
result = con.execute("""
    SELECT * FROM semantic_view(
        'jaffle_orders',
        metrics := ['order_count', 'total_revenue']
    )
""").fetchall()
```
Keep assertion `len(result) == 1`. Update: `order_count = result[0][0]` (int, not needing int() cast — but keep it for safety: `order_count = int(result[0][0])`). Same for `total_revenue`. Assertions on > 0 remain unchanged.

**8. Update Test 4 — Explain:**
Replace `explain_semantic_view` call — it's already correct, but verify the assertion checks for `"jaffle_orders"` and `"raw_orders"` in the explain output. Keep unchanged if already correct.

**9. Add Test 5 — Typed BIGINT output:**
```python
print()
print("Test 5: Typed BIGINT output for count(*) metric")
try:
    result = con.execute("""
        SELECT * FROM semantic_view('jaffle_orders', metrics := ['order_count'])
    """).fetchall()
    assert len(result) == 1, f"Expected 1 row, got {len(result)}"
    assert isinstance(result[0][0], int), \
        f"Expected int (BIGINT) for order_count, got {type(result[0][0])}: {result[0][0]}"
    print(f"  order_count type: {type(result[0][0]).__name__} = {result[0][0]}")
    print("  PASS: count(*) returns BIGINT (Python int)")
    passed += 1
except Exception as e:
    print(f"  FAIL: {e}")
    failed += 1
```

**10. Add Test 6 — Time dimension with day granularity:**
```python
print()
print("Test 6: Time dimension truncated to day granularity")
try:
    import datetime
    result = con.execute("""
        SELECT * FROM semantic_view(
            'jaffle_orders',
            dimensions := ['ordered_at'],
            metrics := ['order_count']
        )
    """).fetchall()
    assert len(result) > 0, "Expected at least one row"
    # All dates should be datetime.date instances
    for row in result:
        assert isinstance(row[0], datetime.date), \
            f"Expected datetime.date for ordered_at, got {type(row[0])}: {row[0]}"
    print(f"  Time dimension rows: {result}")
    print("  PASS: ordered_at returns datetime.date values")
    passed += 1
except Exception as e:
    print(f"  FAIL: {e}")
    failed += 1
```

**11. Update Cleanup:**
Replace `drop_semantic_view` call (it's already correct, just verify):
```python
try:
    con.execute("SELECT drop_semantic_view('jaffle_orders')")
except Exception:
    pass
```

Keep the con.close() and summary unchanged.

**Requirement for docstring update:** Update the docstring at the top to mention:
- v0.2.0 API (create_semantic_view, semantic_view)
- 6 test cases (define, query, aggregate, explain, typed BIGINT, time dimension)
  </action>
  <verify>
    <automated>python3 -c "import ast; ast.parse(open('test/integration/test_ducklake.py').read()); print('syntax OK')" && python3 -c "content = open('test/integration/test_ducklake.py').read(); assert 'define_semantic_view' not in content, 'old API found: define_semantic_view'; assert 'semantic_query' not in content, 'old API found: semantic_query'; assert 'create_semantic_view' in content; assert 'semantic_view' in content; print('API check OK')"</automated>
  </verify>
  <done>test_ducklake.py updated: passes syntax check, uses v0.2.0 API (no define_semantic_view or semantic_query), has 6 test cases, imports from test_ducklake_helpers.</done>
</task>

</tasks>

<verification>
- `grep -n "define_semantic_view\|semantic_query" test/integration/test_ducklake.py` returns nothing
- `grep -n "create_semantic_view\|semantic_view" test/integration/test_ducklake.py` returns hits
- File has 6 test cases (Test 1 through Test 6)
- `from test_ducklake_helpers import` line present
</verification>

<success_criteria>
test_ducklake.py updated to v0.2.0 API. All 4 original test cases preserved and updated. 2 new test cases added (typed BIGINT, time dimension). Imports shared helpers to reduce boilerplate.
</success_criteria>

<output>
After completion, create `.planning/phases/14-ducklake-integration-test-refresh-and-ci-job/14-02-SUMMARY.md`
</output>
