---
summary: "Canvas registerView and importFrom; CanvasObjectKind discriminator on TableInfo and DescribeOptions; SQL gate allowlist refreshed against DuckDB v1.5.x operator names; spatial-extension hardening pre-staged."
breaking: false
---

# 0.8.13 — 2026-05-03

Canvas surface expansion and SQL-gate currency. `registerView` and `importFrom` add the two missing primitives between `registerTable` and `query`; `TableInfo` now distinguishes tables from views via a `kind` discriminator. The plan-walk allowlist is pinned against DuckDB v1.5.2's `PhysicalOperatorToString`, with new operator names that the older long-form list silently missed. Spatial-extension blocks are pre-staged so the moment anyone enables `spatial`, GDAL-backed file readers and rtree internals fail closed.

## Added

- **`CanvasInstance.registerView(name, selectSql, options?)`** and `IDataCanvasProvider.registerView` — register a SQL view on the canvas. The `SELECT` runs through the same four-layer read-only gate `query()` enforces (text deny-list → single-statement → SELECT type → plan-walk allowlist) at registration time, so a malicious definition fails loud at register, not later when the view is referenced. `CREATE OR REPLACE VIEW` semantics: re-registering the same name succeeds; conflict with an existing base table throws `validationError({ reason: 'view_table_clash' })`. `RegisterViewResult` exports `{ viewName, columns }`.
- **`CanvasInstance.importFrom(sourceCanvasId, sourceTableName, options?)`** and `IDataCanvasProvider.importFrom` — copy a table from one canvas into another the caller controls. The lifecycle wrapper validates tenancy on both ids before the provider sees either. Round-trips through a sandbox-rooted Parquet temp file so all column types — including TIMESTAMP/DATE/BLOB — survive losslessly. Idempotent on re-import (drop+create on the target). Defaults `asName` to the source table name. `ImportFromOptions` exports `{ asName?, signal? }`.
- **`CanvasObjectKind` (`'table' | 'view'`)** — exported from `@cyanheads/mcp-ts-core/canvas`. Now present on every `TableInfo` returned by `describe()`. `DescribeOptions.kind` filters results to one kind. `drop()` and `clear()` detect the kind from the catalog and emit the right DDL keyword; `clear()` drops views before tables so a dependent view doesn't block its base table.
- **Spatial-extension pre-staging (issue [#106](https://github.com/cyanheads/mcp-ts-core/issues/106))** — `RTREE_INDEX_SCAN` added to `ALLOWED_PLAN_OPERATORS`; `st_read`, `st_drivers`, `rtree_index_dump` added to `DENIED_TABLE_FUNCTIONS`. Dormant until the `spatial` extension loads (still blocked by `autoinstall_known_extensions=false` / `autoload_known_extensions=false`); the gate is ready the moment anyone opts in.

## Changed

- **`ALLOWED_PLAN_OPERATORS` refreshed against DuckDB v1.5.2 `PhysicalOperatorToString`** — added `TABLE_SCAN`, `CTE_SCAN`, `REC_CTE_SCAN`, `REC_REC_CTE_SCAN`, `DELIM_SCAN`, `POSITIONAL_SCAN`, `LEFT_DELIM_JOIN`, `RIGHT_DELIM_JOIN`, `LIMITED_DISTINCT`, `STREAMING_WINDOW`, `REC_CTE`, `REC_KEY_CTE`. DuckDB stringifies `RECURSIVE_*` as `REC_*`; long names retained for older versions. Pinned with a source link in the comment so future audits know what the list tracks.
- **`assertReadOnlySql` extracted** in `DuckdbProvider` — the four-layer gate that `query()` ran inline is now a private helper, called from both `query()` and `registerView()` so view definitions inherit the same enforcement.
- **`countRows` helper** in `DuckdbProvider` — `SELECT COUNT(*) AS n` + `Number(... ?? 0)` BIGINT-string coercion centralized in one place; called from `query()`, `export()`, `importFrom()`, and `describeOne()`.
- **`TableInfo.rowCount` for views** is materialized at describe time via `COUNT(*)` — not free; treat as an approximation if the view is expensive.
- **vitest** — `silent: 'passed-only'` added to the shared project config so passing-test logs no longer drown signal during local runs.

## Fixed

- **Suppress noisy `Sourcemap for ".../node-cron.js" points to missing source files` warning during `bun run test`** — node-cron ships `dist/` but not `src/`, so Vite's `logger.warnOnce` fires every test run. Patched at the `vitest.config.ts` level to filter just that one stderr line; all other Vite warnings still surface unchanged.
