Execute dynamic DML inside onSchemaChange procedure (#2307) - #2320
Open
apilaskowski wants to merge 3 commits into
Open
apilaskowski wants to merge 3 commits into
apilaskowski wants to merge 3 commits into
Conversation
…or handler The outer EXCEPTION handler emitted by safeCallAndDropProcedure dropped the `_temp` staging table by its unqualified name. That table is scoped to the stored procedure, so it is not resolvable from the calling script, and BigQuery resolves table names before evaluating IF EXISTS. The handler therefore raised Invalid value: Table "<table>_df_temp_<id>_temp" must be qualified with a dataset (e.g. dataset.table) which masked the original error and, because DROP PROCEDURE runs after it, leaked the df_osc_* procedure into the user's dataset. This was most visible with onSchemaChange: "FAIL", where the intended "Schema mismatch defined by on_schema_change = 'FAIL'. Added columns: ..." message never reached the user. The staging table is already dropped inside the procedure by cleanupSql, and BigQuery drops procedure-scoped temporary tables on exit, so the statement was redundant as well as harmful. Removing it also restores parity with the managed Dataform service, whose handler drops only the fully qualified `_empty` table. Adds a regression test asserting that every table dropped in the error handler is fully qualified, and fixes the destructuring in the dynamic-DML test so that the "no static DML after the procedure" assertions inspect the whole trailing script rather than only the CALL block.
apilaskowski
marked this pull request as ready for review
September 18, 2026 19:49
apilaskowski
requested review from
zaptot
and removed request for
a team and
zaptot
September 18, 2026 19:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2307 (together with #2319, which this is stacked on — review that first).
Problem
ExecutionSql.publishTasksemitted the schema-change stored procedure and then fell through to a staticINSERT/MERGE/INSERT_OVERWRITEbuilt fromtableMetadata.fields.tableMetadatais fetched once, before execution, so the DML always used the pre-alteration column list:EXTEND— newly added columns were omitted from theINSERTlist and silently leftNULL.SYNCHRONIZE— dropped columns were still referenced, failing at runtime withUnrecognized name: <dropped_col>.The correct column list only exists after the schema-change DDL has run, i.e. at BigQuery script runtime. It cannot be computed by the CLI at compile time, so the DML has to move inside the procedure.
Solution
Generate the DML dynamically inside the stored procedure, matching the behaviour of the managed Dataform service:
publishTasksnowbreaks instead of falling through, so no static DML is emitted forFAIL/EXTEND/SYNCHRONIZE.IGNOREis unchanged.dataform_columns_list(plusdataform_columns_mergefor merge-strategy tables) is declared up front andSETfromtemp_table_columnsafter the schema-change DDL._tempstaging table.INSERT/MERGE/INSERT_OVERWRITEruns viaEXECUTE IMMEDIATE, splicing in the runtime column lists._emptyand_tempare cleaned up on completion and on error.Generated SQL for
EXTEND— beforeGenerated SQL for
EXTEND— afterBehaviour change
Projects using
EXTENDorSYNCHRONIZEwill see a different generated script. The incremental query is now materialised into a temporary staging table before the DML instead of being inlined into it, so the query text appears twice in the script (once as theLIMIT 0schema probe, once as the staging table).IGNOREis unaffected.Because the procedure queries
INFORMATION_SCHEMA.COLUMNStwice and creates the_emptyprobe table, an incremental run withonSchemaChangeset bills a small fixed amount (~40 MiB observed) even for a trivial query. This matches the managed service.Test coverage
onSchemaChangeINSERT(nouniqueKey)FAIL,EXTENDEXTEND)MERGE(uniqueKey)SYNCHRONIZEINSERT_OVERWRITEEXTENDFAILtrips)FAILcli/api/execution_sql_test.ts): updated goldens foron_schema_change_{fail,extend,synchronize}.sqlandinsert_overwrite_extend.sql; a test asserting the dynamic DML is emitted inside the procedure and no static DML referencing staletableMetadatacolumns is emitted after it; and a test asserting every table dropped in the outer error handler is fully qualified.cli/index_run_e2e_test.ts): theonSchemaChangesuite is upgraded from--dry-runonly to executingEXTENDandSYNCHRONIZElive against BigQuery, asserting via Dataform assertions that added columns (field2) are populated with non-null data and dropped columns (field1) are removed. These require GCP credentials and are taggedintegration(Mark tests that require valid GCP project credentials as integration tests #2289).Verified manually against the exact repro from #2307 (
type: "incremental",onSchemaChange: "FAIL",SELECT 1 AS a) on BigQuery: run 1 creates the table, run 2 succeeds and appends correctly, and changing the query toSELECT 1 AS a, 2 AS bnow fails with the intended message:Reviewer note: intentional commit split
style(cli): format index_run_e2e_test.ts with prettier— mechanical Prettier formatting of pre-existing code incli/index_run_e2e_test.ts, required byscripts/lint, with no logic changes. Kept separate so it does not pollute the functional diff.fix(cli): execute dynamic DML inside onSchemaChange procedure (#2307)— the functional change, goldens, unit test and E2E tests.fix(cli): do not drop the procedure-scoped staging table from the error handler— follow-up fix found while testing this branch live; see below.Reviewing commits 2 and 3 individually gives the cleanest diff.
On commit 3: the outer
EXCEPTIONhandler dropped the_tempstaging table by its unqualified name. That table is scoped to the stored procedure, so it is not resolvable from the calling script, and BigQuery resolves table names before evaluatingIF EXISTS. The handler therefore raisedTable "..._temp" must be qualified with a dataset, which masked the original error and leaked thedf_osc_*procedure (becauseDROP PROCEDUREruns after it). It was most visible withonSchemaChange: "FAIL", where the intended schema-mismatch message never reached the user. The statement was also redundant —cleanupSqlalready drops the table inside the procedure, and BigQuery drops procedure-scoped temp tables on exit.