---
phase: 63
plan: 03
type: execute
wave: 2
depends_on: [63-01]
files_modified:
  - CHANGELOG.md
  - docs/explanation/transactional-ddl-and-limitations.rst
  - docs/reference/create-semantic-view.rst
  - docs/reference/drop-semantic-view.rst
  - docs/reference/alter-semantic-view.rst
  - README.md
  - examples/readonly_load.py
requirements: [DOC-01, DOC-02, DOC-03, DOC-04, DOC-05]
autonomous: true

must_haves:
  truths:
    - "`grep -n '## \\[0.9.0\\]' CHANGELOG.md` returns exactly 1 match."
    - "`grep -n '_explanation-txn-ddl-readonly' docs/explanation/transactional-ddl-and-limitations.rst` returns at least 1 match (label) plus 1 in the Summary cross-reference."
    - "Each of the three reference pages (create/drop/alter-semantic-view.rst) carries a `.. note::` mentioning 'writable database'."
    - "README.md `## Quick start` section mentions read-only support."
    - "`examples/readonly_load.py` exists and runs end-to-end against `build/debug/semantic_views.duckdb_extension`, demonstrating bootstrap → reopen-readonly → query → catch DDL error."
    - "`just docs-check` passes (Sphinx builds with `-W` flag)."
  artifacts:
    - path: "CHANGELOG.md"
      provides: "## [0.9.0] section under Keep-a-Changelog headings + updated compare links"
      contains: "## [0.9.0]"
    - path: "docs/explanation/transactional-ddl-and-limitations.rst"
      provides: "New 'Read-only databases' section with versionadded:: 0.9.0 + companion-file-migration limitation note"
      contains: "Read-only databases"
    - path: "docs/reference/create-semantic-view.rst"
      provides: "One-line note 'Requires a writable database' after Statement Variants"
      contains: "writable database"
    - path: "docs/reference/drop-semantic-view.rst"
      provides: "Same one-line note"
      contains: "writable database"
    - path: "docs/reference/alter-semantic-view.rst"
      provides: "Same one-line note"
      contains: "writable database"
    - path: "README.md"
      provides: "One-line read-only support note in Quick start"
      contains: "read-only"
    - path: "examples/readonly_load.py"
      provides: "End-to-end read-only flow demo (PEP-723 script mirroring transactional_ddl.py)"
      contains: "read_only=True"
  key_links:
    - from: "docs/reference/{create,drop,alter}-semantic-view.rst"
      to: "docs/explanation/transactional-ddl-and-limitations.rst#_explanation-txn-ddl-readonly"
      via: ":ref:`explanation-txn-ddl-readonly`"
      pattern: "explanation-txn-ddl-readonly"
    - from: "docs/explanation/transactional-ddl-and-limitations.rst (Summary)"
      to: "the new Read-only databases section"
      via: "added bullet in 'See also' block"
      pattern: "ref-readonly|read-only databases"
---

<objective>
Land the user-facing documentation and example for v0.9.0:

1. **CHANGELOG.md** — `## [0.9.0]` section under Keep-a-Changelog 1.1.0 headings (`Added`, `Known limitations`); update `[Unreleased]` and bottom-of-file compare links.
2. **`docs/explanation/transactional-ddl-and-limitations.rst`** — new "Read-only databases" section between the existing `_explanation-txn-ddl-drop-alter-race` (ends ~line 119) and `_explanation-txn-ddl-peg` (starts line 122). New label `_explanation-txn-ddl-readonly`. Add cross-reference in the existing Summary block.
3. **Three reference pages** — `docs/reference/{create,drop,alter}-semantic-view.rst` each gain a one-line `.. note::` after their Statement Variants section: "Requires a writable database. On a read-only database this statement fails with DuckDB's standard read-only error. See :ref:`explanation-txn-ddl-readonly`."
4. **README.md** — one-line note in the existing `## Quick start` section (around line 19-60) that read-only databases are supported for queries.
5. **`examples/readonly_load.py`** — full PEP-723 script mirroring `examples/transactional_ddl.py` style, demonstrating: open writable → CREATE view → close → reopen read-only → list/query → attempt DDL → catch read-only error.

Purpose: User-discoverable docs are the gate for declaring v0.9.0 done. The CHANGELOG is rendered into the docs site verbatim; the explanation page is the canonical "why and what" reference; reference pages set caller expectations; README is the discovery surface.

Output: Five .rst/.md/.py files updated/created; `just docs-check` (Sphinx -W) green; `examples/readonly_load.py` runnable.
</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/REQUIREMENTS.md
@.planning/phases/63-readonly-database-load-support/63-RESEARCH.md
@.planning/phases/63-readonly-database-load-support/63-VALIDATION.md
@.planning/phases/63-readonly-database-load-support/63-01-PLAN.md
@CLAUDE.md
@CHANGELOG.md
@docs/explanation/transactional-ddl-and-limitations.rst
@docs/reference/create-semantic-view.rst
@docs/reference/drop-semantic-view.rst
@docs/reference/alter-semantic-view.rst
@README.md
@examples/transactional_ddl.py

<interfaces>
<!-- Existing patterns the executor mirrors -->

CHANGELOG.md current top of file (lines 8-12):
```
<!-- docs-include-start -->

## [Unreleased]

_No unreleased changes yet._

## [0.8.0] - 2026-05-06
```

CHANGELOG.md current bottom of file (lines 254-255):
```
[Unreleased]: https://github.com/anentropic/duckdb-semantic-views/compare/v0.8.0...HEAD
[0.8.0]: https://github.com/anentropic/duckdb-semantic-views/compare/v0.7.2...v0.8.0
```

docs/reference/create-semantic-view.rst — existing two `.. note::` blocks at lines 109-115:
```
.. note::

   Since v0.8.0 all four ``CREATE`` body variants participate in your surrounding transaction. ``BEGIN ... ROLLBACK`` discards an uncommitted ``CREATE``. See :ref:`explanation-transactional-ddl`.

.. note::

   ``CREATE SEMANTIC VIEW IF NOT EXISTS`` reliably absorbs duplicates within a single process or transaction ...
```

docs/explanation/transactional-ddl-and-limitations.rst — current section boundaries:
- _explanation-txn-ddl-drop-alter-race ends ~line 119
- _explanation-txn-ddl-peg starts line 122
- Summary at line 145, "See also:" cross-references at lines 160-165

Existing label-style for the file: `.. _explanation-txn-ddl-{slug}:` (snake-case slug after the prefix).

examples/transactional_ddl.py — full PEP-723 + EXTENSION_PATH env var pattern + scenario fns + `if __name__ == "__main__":` driver. This plan's example follows the same shape.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add `## [0.9.0]` section to CHANGELOG.md and update compare links</name>
  <files>CHANGELOG.md</files>
  <read_first>
    - CHANGELOG.md (full file — top section pattern at lines 8-49 for [Unreleased] + [0.8.0]; bottom compare-link block at lines 254-269)
    - CLAUDE.md (Milestone Completion section: Keep-a-Changelog 1.1.0 headings only, `[Unreleased]` sentinel sentence, compare links pointing to latest tag, no ad-hoc subheadings)
    - .planning/REQUIREMENTS.md (RO-01..RO-05 phrasing — informs the user-facing bullet wording)
  </read_first>
  <action>
    1. **Replace the `## [Unreleased]` section body** (lines 10-12) with a placeholder that points at v0.9.0:

    Current (lines 10-12):
    ```
    ## [Unreleased]

    _No unreleased changes yet._
    ```

    Replace with:
    ```
    ## [Unreleased]

    _No unreleased changes yet._

    ## [0.9.0] - 2026-05-15

    ### Added

    - **Read-only database LOAD support.** ``LOAD semantic_views`` now succeeds on a read-only DuckDB database. Previously-defined semantic views can be queried via ``list_semantic_views()``, ``describe_semantic_view()``, and ``FROM semantic_view(...)`` against a database opened with ``read_only=True`` (Python) or ``--readonly`` (CLI). On a read-only database that was never bootstrapped, the catalog is treated as empty rather than raising a missing-table error: ``list_semantic_views()`` returns zero rows, and ``describe_semantic_view('x')`` / ``FROM semantic_view('x', ...)`` return the standard ``semantic view 'x' does not exist`` error for any name.
    - **Read-only DDL surfaces DuckDB's standard error.** ``CREATE SEMANTIC VIEW``, ``DROP SEMANTIC VIEW``, and ``ALTER SEMANTIC VIEW`` against a read-only database fail with DuckDB's standard ``Cannot execute statement of type "..." on database "..." which is attached in read-only mode!`` error rather than the previous confusing schema-create failure at LOAD time.
    - **`examples/readonly_load.py`** demonstrating the open-writable → bootstrap → close → reopen-readonly → query → catch-DDL-error workflow.
    - **`just test-readonly` recipe + `test/integration/test_readonly_load.py`** Python integration test (three scenarios: fresh read-only file, bootstrapped reopen, DDL rejection) and `test/sql/readonly_load.test` writable-side smoke fixture. Wired into `just test-all`.

    ### Known limitations

    - The v0.1.0 → v0.2.0 companion-file migration cannot run on a read-only database (the migration INSERTs into ``semantic_layer._definitions`` which requires write access). Practical impact is near-zero — the companion-file format is four milestone versions stale and any database last opened with v0.2.0+ has already been migrated. If you have a v0.1.0-era database that has never been opened by any newer release, open it once writable to complete the migration before reverting to read-only.
    ```

    Notes:
    - Date `2026-05-15` matches the planning context — adjust to today's actual date if the executor runs on a different day. Use `date +%Y-%m-%d`.
    - Heading `### Added` and `### Known limitations` ONLY — no ad-hoc subheadings (per CLAUDE.md).
    - Two `### Added` bullets cover the two user-visible facets: the LOAD path (RO-01..RO-04) and DDL behaviour (RO-05). The third bullet covers the example (DOC-05). The fourth covers test infrastructure (TEST-01..TEST-03).

    2. **Update the bottom-of-file compare links** (lines 254-269). Current line 254:

    ```
    [Unreleased]: https://github.com/anentropic/duckdb-semantic-views/compare/v0.8.0...HEAD
    ```

    Replace with the v0.9.0 link AND a new `[Unreleased]` link pointing to v0.9.0...HEAD:

    ```
    [Unreleased]: https://github.com/anentropic/duckdb-semantic-views/compare/v0.9.0...HEAD
    [0.9.0]: https://github.com/anentropic/duckdb-semantic-views/compare/v0.8.0...v0.9.0
    ```

    Leave all other compare-link lines (lines 255-269) untouched.

    Reasoning: per CLAUDE.md, the `[Unreleased]` link MUST point at the latest tag. After Plan 04 tags v0.9.0, this is the correct state.
  </action>
  <verify>
    <automated>grep -nE "^## \[0\.9\.0\] - 2026-" CHANGELOG.md && grep -nE "^\[0\.9\.0\]: " CHANGELOG.md && grep -nE "^\[Unreleased\]: .*v0\.9\.0\.\.\.HEAD$" CHANGELOG.md</automated>
  </verify>
  <acceptance_criteria>
    - `grep -nE "^## \\[0\\.9\\.0\\] - 2026-" CHANGELOG.md` returns exactly 1 match.
    - `grep -nE "^### Added$" CHANGELOG.md` returns at least 2 matches (one new, plus existing 0.8.0/older).
    - `grep -nE "^### Known limitations$" CHANGELOG.md` returns at least 2 matches (new 0.9.0 + existing 0.8.0).
    - `grep -nE "^\\[0\\.9\\.0\\]: " CHANGELOG.md` returns exactly 1 match.
    - `grep -nE "^\\[Unreleased\\]: " CHANGELOG.md` returns exactly 1 match AND points to `v0.9.0...HEAD` (not v0.8.0).
    - The 0.9.0 section uses ONLY standard Keep-a-Changelog headings (`### Added`, `### Known limitations`); no ad-hoc `### Phase 63` style subheadings (verifiable: `grep -nE "^### " CHANGELOG.md | head -20` shows the new entries are only Added / Known limitations under [0.9.0]).
    - Read-only LOAD bullet mentions `LOAD semantic_views`, `list_semantic_views`, `describe_semantic_view`, `semantic_view(...)`.
    - Read-only DDL bullet mentions DuckDB's standard `read-only mode!` error verbatim.
  </acceptance_criteria>
  <done>
    - CHANGELOG `[0.9.0]` section landed under correct headings.
    - Bottom-of-file compare links updated.
    - `[Unreleased]` placeholder text remains as-is (`_No unreleased changes yet._`) above the new section.
  </done>
</task>

<task type="auto">
  <name>Task 2: Insert "Read-only databases" section in transactional-ddl-and-limitations.rst</name>
  <files>docs/explanation/transactional-ddl-and-limitations.rst</files>
  <read_first>
    - docs/explanation/transactional-ddl-and-limitations.rst (lines 100-166 — existing _explanation-txn-ddl-drop-alter-race, _explanation-txn-ddl-peg, Summary, See also block)
    - .planning/phases/63-readonly-database-load-support/63-RESEARCH.md §3 Q7 DOC-02 subsection (recommended insertion point + content outline) and §3 Q2 (companion-file migration limitation)
  </read_first>
  <action>
    Insert a new section between the existing `_explanation-txn-ddl-drop-alter-race` (ends at line 119, before the blank-line + `.. _explanation-txn-ddl-peg:` label at line 122) and the PEG section. The new section must use the label `_explanation-txn-ddl-readonly` so the three reference pages can `:ref:` it.

    Insert this block AFTER line 119 (the line ending `... in this only surfaces if multiple processes ...`) and BEFORE the blank lines preceding `.. _explanation-txn-ddl-peg:`:

    ```rst


    .. _explanation-txn-ddl-readonly:

    Read-Only Databases
    ====================

    .. versionadded:: 0.9.0

    Loading the extension into a read-only DuckDB database works the same way as a writable one — ``LOAD semantic_views`` succeeds and you can query any semantic view that was previously defined. The extension detects ``access_mode = 'read_only'`` at load time and skips the catalog-table bootstrap that would otherwise fail with DuckDB's read-only error.

    Three behaviours change between writable and read-only databases:

    1. **Reads work as usual on a bootstrapped database.** If the database already contains a ``semantic_layer._definitions`` table (because it was opened writable before and one or more semantic views were defined), then ``list_semantic_views()``, ``describe_semantic_view('name')``, ``FROM semantic_view('name', dimensions := [...], metrics := [...])``, and the SHOW / DESCRIBE / GET_DDL family all behave identically to writable mode.

    2. **A fresh read-only database is treated as having zero views, not as an error.** If the database was never bootstrapped (no ``semantic_layer._definitions`` table exists), ``list_semantic_views()`` returns zero rows. ``describe_semantic_view('anything')`` and ``FROM semantic_view('anything', ...)`` return the standard ``semantic view 'anything' does not exist`` error rather than a raw catalog error about a missing table.

    3. **DDL fails with DuckDB's standard read-only error.** ``CREATE``, ``DROP``, and ``ALTER SEMANTIC VIEW`` are rewritten internally into ``INSERT`` / ``DELETE`` / ``UPDATE`` against ``semantic_layer._definitions`` and run on the caller's connection. On a read-only database those statements fail with:

       .. code-block:: text

          Invalid Input Error: Cannot execute statement of type "INSERT" on database "<name>" which is attached in read-only mode!

       The exact statement-type token (``INSERT`` / ``DELETE`` / ``UPDATE``) varies by DDL form. The extension does not wrap or rephrase the message.

    Bootstrap-then-reopen workflow
    ------------------------------

    The typical pattern for shipping a read-only database with pre-defined semantic views is:

    .. code-block:: python

       import duckdb

       # Step 1 — open writable, define views, close.
       rw = duckdb.connect("analytics.duckdb")
       rw.execute("LOAD semantic_views")
       rw.execute(\"\"\"
           CREATE SEMANTIC VIEW orders AS
             TABLES (o AS orders_table PRIMARY KEY (id))
             DIMENSIONS (o.region AS o.region)
             METRICS (o.total AS SUM(o.amount))
       \"\"\")
       rw.close()

       # Step 2 — reopen read-only and query.
       ro = duckdb.connect("analytics.duckdb", read_only=True)
       ro.execute("LOAD semantic_views")
       rows = ro.execute(
           "SELECT * FROM semantic_view('orders', dimensions := ['region'], metrics := ['total'])"
       ).fetchall()

    .. note::

       The v0.1.0 → v0.2.0 companion-file migration cannot run on a read-only database. The migration INSERTs the contents of a sidecar ``<db>.semantic_views`` file into ``semantic_layer._definitions`` and then deletes the sidecar — both writes. If you have a database that was last opened with a release older than v0.2.0 (March 2026) and has never been opened by a newer release, open it once writable to complete the migration before reverting to read-only. Practical impact is near-zero because any database touched by v0.2.0+ has already been migrated.

    ```

    Then update the **Summary** "See also:" block at lines 160-165. Add a new bullet referencing the new section. Current block:

    ```
    See also:

    - :ref:`ref-create-semantic-view` -- syntax for all four ``CREATE`` body forms.
    - :ref:`ref-drop-semantic-view` -- ``DROP`` and ``DROP IF EXISTS``.
    - :ref:`ref-alter-semantic-view` -- ``ALTER`` variants.
    - :ref:`ref-error-messages` -- error catalogue.
    ```

    Replace with (insert new bullet at the top of the list):

    ```
    See also:

    - :ref:`explanation-txn-ddl-readonly` -- read-only database support, bootstrap-then-reopen workflow, and the v0.1.0 migration limitation.
    - :ref:`ref-create-semantic-view` -- syntax for all four ``CREATE`` body forms.
    - :ref:`ref-drop-semantic-view` -- ``DROP`` and ``DROP IF EXISTS``.
    - :ref:`ref-alter-semantic-view` -- ``ALTER`` variants.
    - :ref:`ref-error-messages` -- error catalogue.
    ```

    Notes:
    - Section uses `Read-Only Databases` (capitalised both words; matches the file's existing heading style: `DROP and ALTER Without IF EXISTS Detect Concurrent Drops`).
    - Underline must be at least as long as the heading text (`====================` is 20 chars; heading is 19 chars — sufficient).
    - The `versionadded:: 0.9.0` directive matches the page's existing `versionadded:: 0.8.0` (line 10) — Sphinx supports per-section.
    - The Python code block escapes triple-quoted SQL strings via `\"\"\"` because rST already wraps them in a `code-block:: python` block; only the literal Python triple-quotes need escaping in Markdown contexts. In rST, write the triple quotes literally as `"""` — the executor must NOT escape them. (Inside the rst code block, write `"""` directly.)

    Re-confirm: in the rST source, the Python code block uses literal `"""...""" ` not `\"\"\"`. The escape above was a transcription artefact for the action text — the file content uses literal triple quotes.
  </action>
  <verify>
    <automated>just docs-check 2>&1 | tail -n 30</automated>
  </verify>
  <acceptance_criteria>
    - `grep -nE "^\\.\\. _explanation-txn-ddl-readonly:$" docs/explanation/transactional-ddl-and-limitations.rst` returns exactly 1 match.
    - `grep -n "Read-Only Databases" docs/explanation/transactional-ddl-and-limitations.rst` returns at least 1 match (the heading).
    - `grep -n "versionadded:: 0.9.0" docs/explanation/transactional-ddl-and-limitations.rst` returns exactly 1 match.
    - `grep -n "Bootstrap-then-reopen workflow" docs/explanation/transactional-ddl-and-limitations.rst` returns exactly 1 match.
    - `grep -n "companion-file migration cannot run" docs/explanation/transactional-ddl-and-limitations.rst` returns exactly 1 match.
    - `grep -nE ":ref:\`explanation-txn-ddl-readonly\`" docs/explanation/transactional-ddl-and-limitations.rst` returns exactly 1 match (the Summary cross-reference).
    - The new section appears BEFORE `_explanation-txn-ddl-peg:` label and AFTER `_explanation-txn-ddl-drop-alter-race:` label (verify with `grep -n "^.. _explanation-txn-ddl" docs/explanation/transactional-ddl-and-limitations.rst` showing the order: drop-alter-race, readonly, peg, summary).
    - `just docs-check` exits 0 (Sphinx -W: no warnings, no broken refs).
  </acceptance_criteria>
  <done>
    - New section landed in the correct position with the correct label.
    - Summary cross-reference added at the top of "See also:".
    - `just docs-check` green.
  </done>
</task>

<task type="auto">
  <name>Task 3: Add `.. note::` to each of three reference pages (create/drop/alter)</name>
  <files>docs/reference/create-semantic-view.rst, docs/reference/drop-semantic-view.rst, docs/reference/alter-semantic-view.rst</files>
  <read_first>
    - docs/reference/create-semantic-view.rst (lines 100-120 — existing `.. note::` blocks at lines 109-115)
    - docs/reference/drop-semantic-view.rst (lines 30-50 — existing `.. note::` block at line 34)
    - docs/reference/alter-semantic-view.rst (lines 45-65 — existing `.. note::` block at line 50)
    - .planning/phases/63-readonly-database-load-support/63-RESEARCH.md §3 Q7 DOC-03 subsection (placement: after Statement Variants, mirror existing note shape)
  </read_first>
  <action>
    For each of the three reference pages, add a third `.. note::` admonition AFTER the existing notes that follow the "Statement Variants" section. The text must be identical across all three files (single sentence + cross-reference).

    1. **`docs/reference/create-semantic-view.rst`** — insert after line 115 (end of the existing IF NOT EXISTS race note) and BEFORE the `.. _ref-create-clauses:` label at line 118. Add:

    ```rst

    .. note::

       Requires a writable database. On a read-only database this statement fails with DuckDB's standard ``Cannot execute statement of type "..." which is attached in read-only mode!`` error. See :ref:`explanation-txn-ddl-readonly`.
    ```

    The blank line before the `.. note::` and another blank line after (before `.. _ref-create-clauses:`) are required by rST.

    2. **`docs/reference/drop-semantic-view.rst`** — insert after line 36 (end of the existing concurrent-drop note) and BEFORE the `.. _ref-drop-params:` label at line 39. Add the same `.. note::` block:

    ```rst

    .. note::

       Requires a writable database. On a read-only database this statement fails with DuckDB's standard ``Cannot execute statement of type "..." which is attached in read-only mode!`` error. See :ref:`explanation-txn-ddl-readonly`.
    ```

    3. **`docs/reference/alter-semantic-view.rst`** — insert after line 52 (end of the existing concurrent-drop note) and BEFORE the `.. _ref-alter-params:` label at line 55. Add the same `.. note::` block:

    ```rst

    .. note::

       Requires a writable database. On a read-only database this statement fails with DuckDB's standard ``Cannot execute statement of type "..." which is attached in read-only mode!`` error. See :ref:`explanation-txn-ddl-readonly`.
    ```

    Notes:
    - The `:ref:` target `explanation-txn-ddl-readonly` MUST match the label added in Task 2 (`_explanation-txn-ddl-readonly`). Sphinx accepts the label name without the leading underscore in the `:ref:` syntax.
    - The exact wording is identical across the three files for greppability and consistency. Do NOT vary it per page.
    - The note must come AFTER the existing transactional/concurrent-drop notes and BEFORE the next `.. _ref-*-params:` or `.. _ref-*-clauses:` label so it stays in the "Statement Variants" subsection.
  </action>
  <verify>
    <automated>grep -cn "Requires a writable database" docs/reference/create-semantic-view.rst docs/reference/drop-semantic-view.rst docs/reference/alter-semantic-view.rst && just docs-check 2>&1 | tail -n 30</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "Requires a writable database" docs/reference/create-semantic-view.rst` outputs `1`.
    - `grep -c "Requires a writable database" docs/reference/drop-semantic-view.rst` outputs `1`.
    - `grep -c "Requires a writable database" docs/reference/alter-semantic-view.rst` outputs `1`.
    - `grep -cE ":ref:\`explanation-txn-ddl-readonly\`" docs/reference/create-semantic-view.rst docs/reference/drop-semantic-view.rst docs/reference/alter-semantic-view.rst` outputs `1` for each.
    - `just docs-check` exits 0 (no broken `:ref:` warnings — proves Task 2's label exists and is reachable).
  </acceptance_criteria>
  <done>
    - All three reference pages carry the identical one-line note.
    - All three notes cross-reference the Task 2 explanation section.
    - Sphinx -W build is green.
  </done>
</task>

<task type="auto">
  <name>Task 4: Add read-only mention to README.md Quick start section</name>
  <files>README.md</files>
  <read_first>
    - README.md (lines 19-60 — existing `## Quick start` section, which uses inline SQL examples and has no LOAD heading)
    - .planning/phases/63-readonly-database-load-support/63-RESEARCH.md §3 Q7 DOC-04 subsection (recommendation: add a one-line note in or just after Quick start)
  </read_first>
  <action>
    Insert a one-line note at the END of the `## Quick start` section, immediately AFTER the last code block (line 60) and BEFORE the `## Multi-table (PK/FK relationships)` heading at line 62.

    Insert this block (with surrounding blank lines):

    ```markdown

    > **Read-only databases:** queries (`semantic_view`, `list_semantic_views`, `describe_semantic_view`, etc.) work against a database opened with `read_only=True`. `CREATE` / `DROP` / `ALTER SEMANTIC VIEW` require a writable database. See the [transactional DDL and limitations](https://anentropic.github.io/duckdb-semantic-views/explanation/transactional-ddl-and-limitations.html#read-only-databases) explanation page for the bootstrap-then-reopen workflow.

    ```

    Notes:
    - Markdown blockquote (`>`) format matches GitHub README conventions for callouts; the README has no existing `.. note::` style admonitions because it is rendered by GitHub, not Sphinx.
    - The link target uses the docs-site URL with the anchor `#read-only-databases`, which is what Sphinx generates from the `Read-Only Databases` heading added in Task 2 (Sphinx slugifies `Read-Only Databases` → `read-only-databases`).
    - One blank line before `>` (separation from preceding code block fence) and one blank line after (separation from `## Multi-table` heading).
    - Text mentions BOTH the supported reads AND the writability constraint for DDL — covers DOC-04's "mentions read-only support" requirement.
  </action>
  <verify>
    <automated>grep -nE "Read-only databases" README.md && grep -nE "read_only=True" README.md</automated>
  </verify>
  <acceptance_criteria>
    - `grep -n "Read-only databases" README.md` returns exactly 1 match.
    - `grep -nE "read_only=True" README.md` returns at least 1 match.
    - `grep -n "transactional-ddl-and-limitations" README.md` returns at least 1 match (the docs-site link).
    - The new block appears AFTER line 60 (end of Quick start last code block) and BEFORE the `## Multi-table` heading. Verify with `grep -n "^## " README.md` showing the heading order is unchanged (How it works, Quick start, Multi-table, ...).
    - `git diff README.md | grep "^+" | grep -v "^+++"` shows only the new lines added (no accidental edits elsewhere).
  </acceptance_criteria>
  <done>
    - One-line read-only callout landed at the end of Quick start.
    - Link to the explanation page is present.
    - No other README content changed.
  </done>
</task>

<task type="auto">
  <name>Task 5: Create examples/readonly_load.py end-to-end demo</name>
  <files>examples/readonly_load.py</files>
  <read_first>
    - examples/transactional_ddl.py (full file — exact PEP-723 metadata block, EXTENSION_PATH env var pattern, scenario function shape, `if __name__ == "__main__":` driver)
    - .planning/phases/63-readonly-database-load-support/63-RESEARCH.md §3 Q7 DOC-05 subsection (suggested narrative: writable bootstrap → close → reopen-readonly → list/query → DDL → catch error)
  </read_first>
  <action>
    Create `examples/readonly_load.py` mirroring `examples/transactional_ddl.py`'s structure. Use ONLY `duckdb==1.5.2` as a dependency (no ADBC needed).

    ```python
    #!/usr/bin/env python3
    # /// script
    # dependencies = ["duckdb==1.5.2"]
    # requires-python = ">=3.10"
    # ///
    """
    uv run examples/readonly_load.py

    Demonstrates v0.9.0 features:
      - LOAD semantic_views works on a read-only database.
      - Previously-defined semantic views can be queried via list_semantic_views,
        describe_semantic_view, and the semantic_view table function.
      - CREATE / DROP / ALTER SEMANTIC VIEW fail with DuckDB's standard
        read-only error rather than a confusing schema-create error at LOAD.

    Two scenarios:
      1. Bootstrap a view in a writable connection, close, reopen the same
         file with read_only=True, and query the view.
      2. Open a fresh (never-bootstrapped) database read-only and confirm
         list_semantic_views() returns an empty list (no error) and
         describe_semantic_view('missing') surfaces the standard
         "does not exist" error.

    Both scenarios end by attempting a CREATE on the read-only connection
    to demonstrate the read-only error message verbatim.
    """

    import os
    import tempfile

    import duckdb

    EXTENSION_PATH = os.environ.get(
        "SEMANTIC_VIEWS_EXTENSION_PATH",
        "build/debug/semantic_views.duckdb_extension",
    )


    SETUP_SQL = """
    CREATE TABLE orders (
        id INTEGER PRIMARY KEY,
        region VARCHAR,
        amount DECIMAL(10,2)
    );
    INSERT INTO orders VALUES
        (1, 'US', 100.00),
        (2, 'EU', 200.00),
        (3, 'US', 150.00);
    """


    CREATE_VIEW_SQL = """
    CREATE SEMANTIC VIEW orders_view AS
      TABLES (o AS orders PRIMARY KEY (id))
      DIMENSIONS (o.region AS o.region)
      METRICS (o.total AS SUM(o.amount))
    """


    def list_views(con) -> list[str]:
        rows = con.execute("SELECT name FROM list_semantic_views()").fetchall()
        return [r[0] for r in rows]


    def bootstrapped_demo() -> None:
        print("=== Scenario 1: bootstrapped database, reopened read-only ===\n")
        with tempfile.TemporaryDirectory(prefix="sv_readonly_demo_") as tmp:
            db_path = os.path.join(tmp, "demo.duckdb")

            # Step 1 — open writable, install + load extension, define a view.
            rw = duckdb.connect(db_path, config={"allow_unsigned_extensions": "true"})
            rw.execute(f"LOAD '{EXTENSION_PATH}'")
            rw.execute(SETUP_SQL)
            rw.execute(CREATE_VIEW_SQL)
            print("Defined views (writable):", list_views(rw))
            rw.close()

            # Step 2 — reopen the SAME file read-only and LOAD the extension.
            # Pre-v0.9.0 this LOAD raised "Cannot execute statement of type
            # CREATE_SCHEMA on database ... in read-only mode" because
            # init_catalog unconditionally issued CREATE SCHEMA IF NOT EXISTS.
            ro = duckdb.connect(
                db_path,
                read_only=True,
                config={"allow_unsigned_extensions": "true"},
            )
            ro.execute(f"LOAD '{EXTENSION_PATH}'")
            print("LOAD on read-only DB:    OK\n")

            # Step 3 — query.
            print("list_semantic_views:    ", list_views(ro))
            desc = ro.execute("FROM describe_semantic_view('orders_view')").fetchall()
            print(f"describe rows:           {len(desc)} metadata rows")
            rows = ro.execute(
                "SELECT region, total FROM semantic_view("
                "  'orders_view', dimensions := ['region'], metrics := ['total']"
                ") ORDER BY region"
            ).fetchall()
            print("semantic_view rows:     ", rows)

            # Step 4 — DDL fails with DuckDB's standard read-only error.
            print()
            try:
                ro.execute("DROP SEMANTIC VIEW orders_view")
                print("DROP unexpectedly succeeded -- this should not happen.")
            except duckdb.Error as e:
                print(f"DROP on read-only DB:    fails as expected\n  -> {e}")

            ro.close()
            print()


    def fresh_readonly_demo() -> None:
        print("=== Scenario 2: fresh read-only database, no bootstrap ===\n")
        with tempfile.TemporaryDirectory(prefix="sv_fresh_ro_demo_") as tmp:
            db_path = os.path.join(tmp, "fresh.duckdb")

            # Bootstrap a valid DuckDB file header by opening writable
            # WITHOUT loading the extension, then closing.
            duckdb.connect(db_path).execute("SELECT 1").close()

            ro = duckdb.connect(
                db_path,
                read_only=True,
                config={"allow_unsigned_extensions": "true"},
            )
            ro.execute(f"LOAD '{EXTENSION_PATH}'")
            print("LOAD on fresh read-only DB:    OK")
            print("list_semantic_views (no bootstrap):", list_views(ro))
            print("  -> empty list, NOT a catalog error\n")

            # describe_semantic_view on a missing view -> clean "does not exist".
            try:
                ro.execute("FROM describe_semantic_view('nonexistent')").fetchall()
            except duckdb.Error as e:
                print(f"describe_semantic_view('nonexistent') -> {e}")

            # CREATE on a fresh read-only DB also fails (the rewrite emits an
            # INSERT against semantic_layer._definitions which doesn't exist).
            print()
            try:
                ro.execute(CREATE_VIEW_SQL.replace("orders_view", "v"))
                print("CREATE unexpectedly succeeded -- this should not happen.")
            except duckdb.Error as e:
                print(f"CREATE on fresh read-only DB:    fails as expected\n  -> {e}")

            ro.close()


    if __name__ == "__main__":
        bootstrapped_demo()
        fresh_readonly_demo()
        print("\nAll scenarios completed.")
    ```

    Notes:
    - PEP-723 metadata mirrors `transactional_ddl.py:1-9` minus ADBC.
    - `EXTENSION_PATH` env var with default `build/debug/semantic_views.duckdb_extension` matches `transactional_ddl.py:40-43`.
    - `tempfile.TemporaryDirectory(prefix="sv_readonly_demo_")` matches `transactional_ddl.py:129`.
    - Both scenarios deliberately demonstrate FAILURE (the DDL try/except blocks) — that is the user-facing demo of RO-05.
    - The fresh-DB bootstrap pattern (`duckdb.connect(db_path).execute("SELECT 1").close()`) ensures DuckDB writes a valid file header before the read-only reopen (RESEARCH §9 Risk 5).
  </action>
  <verify>
    <automated>just build && uv run examples/readonly_load.py 2>&1 | tail -n 30</automated>
  </verify>
  <acceptance_criteria>
    - File `examples/readonly_load.py` exists.
    - `grep -nE "^# /// script$" examples/readonly_load.py` returns 1 match.
    - `grep -nE 'duckdb==1\.5\.2' examples/readonly_load.py` returns 1 match.
    - `grep -nE "read_only=True" examples/readonly_load.py` returns at least 2 matches (one per scenario).
    - `grep -n "def bootstrapped_demo" examples/readonly_load.py` returns 1 match.
    - `grep -n "def fresh_readonly_demo" examples/readonly_load.py` returns 1 match.
    - `uv run examples/readonly_load.py` exits 0; output contains lines `LOAD on read-only DB:    OK`, `LOAD on fresh read-only DB:    OK`, and `All scenarios completed.`.
    - Output for both `DROP on read-only DB:` and `CREATE on fresh read-only DB:` shows `fails as expected` with a DuckDB error containing the substring `read-only` or `does not exist`.
  </acceptance_criteria>
  <done>
    - Example runs end-to-end against the Plan 01 extension binary.
    - Demonstrates the four user-visible Phase 63 behaviours (LOAD on read-only OK, query OK, fresh DB empty list, DDL fails) in one script.
  </done>
</task>

<task type="auto">
  <name>Task 6: Re-run `just docs-check` and `just ci` to confirm no doc warnings or regressions</name>
  <files></files>
  <read_first>
    - All five files modified in Tasks 1-5 (final state)
    - CLAUDE.md (Quality Gate: `just ci` includes `docs-check`)
    - feedback_no_tail_on_long_commands (redirect long commands to $TMPDIR)
  </read_first>
  <action>
    Run, IN FOREGROUND, redirecting to `$TMPDIR` per `feedback_no_tail_on_long_commands`:

    1. `just docs-check > $TMPDIR/phase63_docs_check.log 2>&1; STATUS=$?; tail -n 40 $TMPDIR/phase63_docs_check.log; exit $STATUS` — must exit 0. Sphinx -W flag means any warning fails the build (broken refs, duplicate labels, malformed rST, etc.).

    2. `uv run examples/readonly_load.py > $TMPDIR/phase63_example.log 2>&1; STATUS=$?; tail -n 40 $TMPDIR/phase63_example.log; exit $STATUS` — must exit 0. Confirms the example still runs after any cross-file edits.

    3. `just ci > $TMPDIR/phase63_ci_after_docs.log 2>&1; STATUS=$?; tail -n 80 $TMPDIR/phase63_ci_after_docs.log; exit $STATUS` — must exit 0. Full quality gate including lint + test-all + check-fuzz + docs-check.

    If `just docs-check` fails:
    - Look for `WARNING: undefined label` (broken `:ref:` — verify Task 2's label exactly matches the cross-references in Tasks 2/3).
    - Look for `WARNING: Title underline too short` (rST heading underlines must be ≥ heading text length).
    - Look for `ERROR: Unexpected indentation` (rST is whitespace-sensitive — ensure blank lines around code-blocks and notes).

    If `just ci` fails on lint:
    - `cargo fmt --check` failure → run `cargo fmt`.
    - `cargo clippy` failure → fix the warning (no `#[allow]` shortcuts).

    If `just ci` fails on `test-all`:
    - Plan 02 should already have addressed this. If this task surfaces a failure, it means the doc-only changes in this plan accidentally touched a tested file — review the diff.
  </action>
  <verify>
    <automated>just docs-check > $TMPDIR/phase63_docs_check_verify.log 2>&1; STATUS=$?; tail -n 30 $TMPDIR/phase63_docs_check_verify.log; exit $STATUS</automated>
  </verify>
  <acceptance_criteria>
    - `just docs-check` exits 0 (Sphinx -W: zero warnings).
    - `uv run examples/readonly_load.py` exits 0.
    - `just ci` exits 0 (full quality gate passes).
    - Branch is still `milestone/v0.9.0` (`git branch --show-current`).
  </acceptance_criteria>
  <done>
    - Documentation builds cleanly with the new section + cross-references.
    - Example script runs cleanly.
    - Full CI quality gate green.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Doc cross-references → Sphinx build | Bad `:ref:` targets fail `docs-check -W`; surfaced before merge. |
| Example script → user environment | Reads `SEMANTIC_VIEWS_EXTENSION_PATH` env var; falls back to `build/debug/...`. No network calls, no file writes outside `tempfile.TemporaryDirectory()`. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-63-D1 | Information Disclosure | Example writes a temp DB containing a synthetic semantic view; on crash the temp file may persist | accept | `tempfile.TemporaryDirectory()` cleanup runs on context exit even on uncaught exceptions; only synthetic data, no PII. |
| T-63-D2 | Tampering | Doc cross-reference `:ref:`explanation-txn-ddl-readonly` mismatches the label name | mitigate | Task 6 runs `just docs-check` with Sphinx `-W` flag — undefined labels fail the build. |
| T-63-D3 | Spoofing | README link to docs site uses HTTPS GitHub Pages URL; no third-party redirector | mitigate | Link is to `anentropic.github.io` (the project's own canonical docs URL, used elsewhere in README). |
| T-63-D4 | Repudiation | CHANGELOG date may not match the actual release date | mitigate | Task 1 instructs the executor to use `date +%Y-%m-%d` if the planning context date doesn't match. Plan 04 (release) re-confirms the date matches the tag day. |
</threat_model>

<verification>
After all tasks complete:

1. `grep -n "## \[0.9.0\]" CHANGELOG.md` — exactly 1 match.
2. `grep -n "_explanation-txn-ddl-readonly" docs/explanation/transactional-ddl-and-limitations.rst` — at least 2 matches (label + Summary cross-ref).
3. `grep -c "Requires a writable database" docs/reference/{create,drop,alter}-semantic-view.rst` — 1 each.
4. `grep -n "Read-only databases" README.md` — exactly 1 match.
5. `uv run examples/readonly_load.py` — exits 0.
6. `just docs-check` — exits 0.
7. `just ci` — exits 0.
8. `git branch --show-current` outputs `milestone/v0.9.0`.
</verification>

<success_criteria>
- CHANGELOG `[0.9.0]` section under correct Keep-a-Changelog headings.
- New explanation section with `versionadded:: 0.9.0` directive and the migration limitation.
- Three reference pages cross-reference the new section.
- README has a discoverable read-only callout.
- Working `examples/readonly_load.py`.
- `just docs-check` and `just ci` both green.
</success_criteria>

<output>
After completion, create `.planning/phases/63-readonly-database-load-support/63-03-SUMMARY.md` documenting:
- Five files modified/created with line counts.
- Confirmation `just docs-check` and `just ci` green.
- Confirmation `examples/readonly_load.py` runs end-to-end.
- Branch verified as `milestone/v0.9.0`.
- Hand-off: Plan 04 (release) bumps versions and re-runs `just ci` for the final pre-tag green build.
</output>
