cockroachdb / cockroachdb/cockroach
sql: internal planners lack session state used by routines and builtins
- Dominant language
- Go
- Stars
- 32.5k
- Forks
- 4.1k
- PR merge metrics
- PR metrics pending
Description
**Describe the problem**
A query that works when run from a client session can fail when the same query is executed by the schema changer's backfill for `CREATE TABLE ... AS` or `CREATE MATERIALIZED VIEW`. The backfill runs the query on a planner created with `NewInternalPlanner`, which is not backed by a connExecutor. That planner leaves session-owned state either nil (`PreparedStatementState`, the schema changer state) or as an empty placeholder (the cursor store, savepoint accessor, and prepared statement store). Anything that reaches for that state fails, in several cases with a nil pointer dereference:
- A PL/pgSQL block with an `EXCEPTION` handler panics in `SchemaChangerState.makeSnapshot`, reached from `routineGenerator.maybeInitBlockState`. The job fails and reverts.
- A PL/pgSQL routine that opens a cursor fails with `cursor "" already in use`, because the placeholder cursor store generates no unique names.
- `crdb_internal.serialize_session()` panics in `serializeSessionState` on the nil `PreparedStatementState`.
By contrast, the eval contexts built for remote DistSQL flows and for the schema changer's column and index backfills install the `faketreeeval` dummy implementations, which fail with a clear "cannot run without full session context" error. `NewInternalPlanner` is a real planner with holes, so it crashes instead.
Ordinary PL/pgSQL functions, SQL-language UDFs, and the planner-backed builtins that were checked (privilege and role checks including membership expansion as a non-admin, `set_config`, sequence functions, advisory locks, catalog helpers, constraint revalidation, plan gist decoding, zone config lookups, fingerprinting) all work under the backfill, because the interfaces they use are wired.
**To Reproduce**
```sql
CREATE TABLE src (i INT);
INSERT INTO src VALUES (1), (2), (3);
CREATE FUNCTION f(i INT) RETURNS INT AS $$
BEGIN
BEGIN
IF i = 2 THEN SELECT 1 // 0; END IF;
EXCEPTION WHEN division_by_zero THEN
RETURN -1;
END;
RETURN i;
END
$$ LANGUAGE PLpgSQL;
SELECT f(i) FROM src; -- works: 1, -1, 3
CREATE TABLE dst AS SELECT f(i) AS v FROM src; -- internal error: nil pointer dereference
CREATE MATERIALIZED VIEW mv AS SELECT f(i) AS v FROM src; -- same
CREATE FUNCTION g(i INT) RETURNS INT AS $$
DECLARE c CURSOR FOR SELECT i; v INT;
BEGIN OPEN c; FETCH c INTO v; CLOSE c; RETURN v; END
$$ LANGUAGE PLpgSQL;
SELECT g(i) FROM src; -- works
CREATE TABLE dst2 AS SELECT g(i) AS v FROM src; -- ERROR: cursor "" already in use
CREATE TABLE dst3 AS SELECT crdb_internal.serialize_session() FROM src; -- internal error: nil pointer dereference
```
**Expected behavior**
Either the internal planner carries the session state these paths need, or it installs error-returning placeholders like the `faketreeeval` dummies so that every such path fails with a clear error rather than a crash.
**Environment**
master as of 2026-09-08, single-node local cluster, `cockroach sql`.
**Additional context**
Found while routing savepoint operations through the connExecutor (cockroachlabs/cockroach#4325): the savepoint the exception handler takes now goes through the planner's savepoint accessor, which turns the panic above into an assertion failure on the same statement.
Jira issue: CRDB-68069
Contributor guide
Research direction
Start at NewInternalPlanner and compare its session-state wiring with the faketreeeval implementations used by remote DistSQL and schema-changer backfills. Trace PreparedStatementState, SchemaChangerState, the cursor and savepoint accessors, and prepared-statement storage through connExecutor; done means the listed CREATE TABLE AS and CREATE MATERIALIZED VIEW cases either work or return clear errors without nil dereferences or cursor collisions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 44/100