Index: src/alter.c ================================================================== --- src/alter.c +++ src/alter.c @@ -525,10 +525,12 @@ char *zOld = 0; /* Old column name */ char *zNew = 0; /* New column name */ const char *zDb; /* Name of schema containing the table */ int iSchema; /* Index of the schema */ int bQuote; /* True to quote the new name */ + Vdbe *v; /* VDBE handle */ + int regRefCnt; /* Register for number of refs to column */ /* Locate the table to be altered */ pTab = sqlite3LocateTableItem(pParse, 0, &pSrc->a[0]); if( !pTab ) goto exit_rename_column; @@ -558,41 +560,85 @@ if( iCol==pTab->nCol ){ sqlite3ErrorMsg(pParse, "no such column: \"%s\"", zOld); goto exit_rename_column; } - /* Do the rename operation using a recursive UPDATE statement that - ** uses the sqlite_rename_column() SQL function to compute the new - ** CREATE statement text for the sqlite_master table. - */ + /* Allocate a register to count the number of rewritten column refs. */ + v = sqlite3GetVdbe(pParse); + if( v==0 ) goto exit_rename_column; + regRefCnt = ++pParse->nMem; + sqlite3VdbeAddOp2(v, OP_Integer, 0, regRefCnt); + + /* Do the rename operation using an UPDATE statement that uses the + ** sqlite_rename_column() SQL function to compute the new CREATE + ** statement text for the sqlite_master table. */ sqlite3MayAbort(pParse); zNew = sqlite3NameFromToken(db, pNew); if( !zNew ) goto exit_rename_column; assert( pNew->n>0 ); bQuote = sqlite3Isquote(pNew->z[0]); sqlite3NestedParse(pParse, "UPDATE \"%w\".%s SET " - "sql = sqlite_rename_column(sql, type, name, %Q, %Q, %d, %Q, %d, %d) " + "sql = sqlite_rename_column(sql, type, name, %Q, %Q, %d, %Q, %d, %d, %d) " "WHERE name NOT LIKE 'sqliteX_%%' ESCAPE 'X' " " AND (type != 'index' OR tbl_name = %Q)" " AND sql NOT LIKE 'create virtual%%'", zDb, MASTER_NAME, - zDb, pTab->zName, iCol, zNew, bQuote, iSchema==1, + zDb, pTab->zName, iCol, zNew, bQuote, iSchema==1, regRefCnt, pTab->zName ); - sqlite3NestedParse(pParse, - "UPDATE temp.%s SET " - "sql = sqlite_rename_column(sql, type, name, %Q, %Q, %d, %Q, %d, 1) " - "WHERE type IN ('trigger', 'view')", - MASTER_NAME, - zDb, pTab->zName, iCol, zNew, bQuote - ); + if( iSchema!=1 ){ + sqlite3NestedParse(pParse, + "UPDATE temp.%s SET " + "sql = sqlite_rename_column(sql, type, name, %Q, %Q, %d, %Q, %d, 1, %d)" + "WHERE type IN ('trigger', 'view')", + MASTER_NAME, + zDb, pTab->zName, iCol, zNew, bQuote, regRefCnt + ); + } /* Drop and reload the database schema. */ renameReloadSchema(pParse, iSchema); - renameTestSchema(pParse, zDb, iSchema==1); + + /* Now arrange for the sqlite_rename_column() function to be called a + ** second time on the already rewritten schema. This serves two purposes: + ** (a) to check that the schema can still be parsed, and (b) to decrement + ** register regRefCnt once for each reference to the renamed column in the + ** schema (the calls above incremented regRefCnt once for each such reference + ** prior to renaming). If register regRefCnt is not left set to 0, then + ** renaming the column has left the schema with either more or fewer + ** references to the column than there was at the beginning of the + ** operation. This is an error, so return an error message and SQLITE_ABORT + ** the statement. */ + sqlite3NestedParse(pParse, + "SELECT 1 " + "FROM \"%w\".%s " + "WHERE name NOT LIKE 'sqliteX_%%' ESCAPE 'X'" + " AND sql NOT LIKE 'create virtual%%'" + " AND (type != 'index' OR tbl_name = %Q)" + " AND sqlite_rename_column(sql,type,name,%Q,%Q,%d,%Q,%d,%d,%d)=NULL", + zDb, MASTER_NAME, + pTab->zName, + zDb, pTab->zName, iCol, zNew, bQuote, iSchema==1, regRefCnt*-1 + ); + if( iSchema!=1 ){ + sqlite3NestedParse(pParse, + "SELECT 1 " + "FROM temp.%s " + "WHERE type IN ('trigger', 'view')" + " AND sqlite_rename_column(sql,type,name,%Q,%Q,%d,%Q,%d,1,%d)=NULL", + MASTER_NAME, + zDb, pTab->zName, iCol, zNew, bQuote, regRefCnt*-1 + ); + } + + sqlite3VdbeAddOp2(v, OP_IfNot, regRefCnt, sqlite3VdbeCurrentAddr(v)+2); + sqlite3VdbeAddOp2(v, OP_Halt, SQLITE_ERROR, OE_Abort); + sqlite3VdbeChangeP4(v, -1, + "schema contains ambiguous double-quoted string", P4_STATIC + ); exit_rename_column: sqlite3SrcListDelete(db, pSrc); sqlite3DbFree(db, zOld); sqlite3DbFree(db, zNew); @@ -1223,11 +1269,13 @@ } /* ** SQL function: ** -** sqlite_rename_column(zSql, iCol, bQuote, zNew, zTable, zOld) +** sqlite_rename_column(...) +** +** The function requires exactly 10 arguments: ** ** 0. zSql: SQL statement to rewrite ** 1. type: Type of object ("table", "view" etc.) ** 2. object: Name of object ** 3. Database: Database name (e.g. "main") @@ -1234,18 +1282,23 @@ ** 4. Table: Table name ** 5. iCol: Index of column to rename ** 6. zNew: New column name ** 7. bQuote: Non-zero if the new column name should be quoted. ** 8. bTemp: True if zSql comes from temp schema +** 9. iReg: Register to increment for each rewritten column ref. Or, +** if this parameter is negative, -1 times the id of a register +** to decrement for each rewritten column ref. ** ** Do a column rename operation on the CREATE statement given in zSql. ** The iCol-th column (left-most is 0) of table zTable is renamed from zCol ** into zNew. The name should be quoted if bQuote is true. ** ** This function is used internally by the ALTER TABLE RENAME COLUMN command. ** It is only accessible to SQL created using sqlite3NestedParse(). It is -** not reachable from ordinary SQL passed into sqlite3_prepare(). +** not reachable from ordinary SQL passed into sqlite3_prepare(). Except, +** this function is reachable for testing purposes if the +** SQLITE_TESTCTRL_INTERNAL_FUNCTIONS test-control is engaged. */ static void renameColumnFunc( sqlite3_context *context, int NotUsed, sqlite3_value **argv @@ -1257,10 +1310,11 @@ const char *zTable = (const char*)sqlite3_value_text(argv[4]); int iCol = sqlite3_value_int(argv[5]); const char *zNew = (const char*)sqlite3_value_text(argv[6]); int bQuote = sqlite3_value_int(argv[7]); int bTemp = sqlite3_value_int(argv[8]); + int regRefCnt = sqlite3_value_int(argv[9]); const char *zOld; int rc; Parse sParse; Walker sWalker; Index *pIdx; @@ -1365,11 +1419,10 @@ renameColumnElistNames(&sParse, &sCtx, pStep->pExprList, zOld); } } } - /* Find tokens to edit in UPDATE OF clause */ if( sParse.pTriggerTab==pTab ){ renameColumnIdlistNames(&sParse, &sCtx,sParse.pNewTrigger->pColumns,zOld); } @@ -1376,16 +1429,20 @@ /* Find tokens to edit in various expressions and selects */ renameWalkTrigger(&sWalker, sParse.pNewTrigger); } assert( rc==SQLITE_OK ); + if( regRefCnt!=0 ){ + int mul = (regRefCnt<0 ? -1 : 1); + sqlite3VdbeIncrReg(context, regRefCnt*mul, sCtx.nList*mul); + } rc = renameEditSql(context, &sCtx, zSql, zNew, bQuote); renameColumnFunc_done: if( rc!=SQLITE_OK ){ if( sParse.zErrMsg ){ - renameColumnParseError(context, 0, argv[1], argv[2], &sParse); + renameColumnParseError(context, regRefCnt<0, argv[1], argv[2], &sParse); }else{ sqlite3_result_error_code(context, rc); } } @@ -1657,12 +1714,12 @@ /* ** Register built-in functions used to help implement ALTER TABLE */ void sqlite3AlterFunctions(void){ static FuncDef aAlterTableFuncs[] = { - INTERNAL_FUNCTION(sqlite_rename_column, 9, renameColumnFunc), + INTERNAL_FUNCTION(sqlite_rename_column, 10, renameColumnFunc), INTERNAL_FUNCTION(sqlite_rename_table, 7, renameTableFunc), INTERNAL_FUNCTION(sqlite_rename_test, 5, renameTableTest), }; sqlite3InsertBuiltinFuncs(aAlterTableFuncs, ArraySize(aAlterTableFuncs)); } #endif /* SQLITE_ALTER_TABLE */ Index: src/resolve.c ================================================================== --- src/resolve.c +++ src/resolve.c @@ -816,11 +816,13 @@ if( (pDef->funcFlags & SQLITE_FUNC_INTERNAL)!=0 && pParse->nested==0 && sqlite3Config.bInternalFunctions==0 ){ /* Internal-use-only functions are disallowed unless the - ** SQL is being compiled using sqlite3NestedParse() */ + ** SQL is being compiled using sqlite3NestedParse(). The + ** SQLITE_TESTCTRL_INTERNAL_FUNCTIONS test-control will override + ** this restriction for testing purposes. */ no_such_func = 1; pDef = 0; } } Index: src/vdbe.h ================================================================== --- src/vdbe.h +++ src/vdbe.h @@ -281,10 +281,11 @@ #ifndef SQLITE_OMIT_TRIGGER void sqlite3VdbeLinkSubProgram(Vdbe *, SubProgram *); #endif int sqlite3NotPureFunc(sqlite3_context*); +void sqlite3VdbeIncrReg(sqlite3_context*, int, int); /* Use SQLITE_ENABLE_COMMENTS to enable generation of extra comments on ** each VDBE opcode. ** ** Use the SQLITE_ENABLE_MODULE_COMMENTS macro to see some extra no-op Index: src/vdbeaux.c ================================================================== --- src/vdbeaux.c +++ src/vdbeaux.c @@ -4907,10 +4907,20 @@ -1); return 0; } return 1; } + +/* +** Increment the value of register iReg, which is guaranteed to be an +** integer, in the VM associated with context object pCtx by iVal. +*/ +void sqlite3VdbeIncrReg(sqlite3_context *pCtx, int iReg, int iVal){ + Mem *pMem = &pCtx->pVdbe->aMem[iReg]; + assert( pMem->flags==MEM_Int ); + pMem->u.i += iVal; +} #ifndef SQLITE_OMIT_VIRTUALTABLE /* ** Transfer error message text from an sqlite3_vtab.zErrMsg (text stored ** in memory obtained from sqlite3_malloc) into a Vdbe.zErrMsg (text stored Index: test/alter.test ================================================================== --- test/alter.test +++ test/alter.test @@ -684,11 +684,11 @@ # alter-9.X - Special test: Make sure the sqlite_rename_column() and # rename_table() functions do not crash when handed bad input. # sqlite3_test_control SQLITE_TESTCTRL_INTERNAL_FUNCTIONS 1 do_test alter-9.1 { - execsql {SELECT SQLITE_RENAME_COLUMN(0,0,0,0,0,0,0,0,0)} + execsql {SELECT SQLITE_RENAME_COLUMN(0,0,0,0,0,0,0,0,0,0)} } {{}} foreach {tn sql} { 1 { SELECT SQLITE_RENAME_TABLE(0,0,0,0,0,0,0) } 2 { SELECT SQLITE_RENAME_TABLE(10,20,30,40,50,60,70) } 3 { SELECT SQLITE_RENAME_TABLE('foo','foo','foo','foo','foo','foo','foo') } Index: test/alterauth2.test ================================================================== --- test/alterauth2.test +++ test/alterauth2.test @@ -80,11 +80,10 @@ ALTER TABLE t2 RENAME a TO aaa; } { {SQLITE_ALTER_TABLE main t2 {} {}} {SQLITE_FUNCTION {} like {} {}} {SQLITE_FUNCTION {} sqlite_rename_column {} {}} - {SQLITE_FUNCTION {} sqlite_rename_test {} {}} {SQLITE_READ sqlite_master name main {}} {SQLITE_READ sqlite_master sql main {}} {SQLITE_READ sqlite_master tbl_name main {}} {SQLITE_READ sqlite_master type main {}} {SQLITE_READ sqlite_temp_master name temp {}} Index: test/altercol.test ================================================================== --- test/altercol.test +++ test/altercol.test @@ -636,21 +636,21 @@ ); } {} do_execsql_test 14.2 { SELECT - sqlite_rename_column(sql, type, object, db, tbl, icol, znew, bquote, 0) + sqlite_rename_column(sql, type, object, db, tbl, icol, znew, bquote, 0, 0) FROM ddd; } {{} {} {} {}} sqlite3_test_control SQLITE_TESTCTRL_INTERNAL_FUNCTIONS 0 # If the INTERNAL_FUNCTIONS test-control is disabled (which is the default) # then the sqlite_rename_table() SQL function is not accessible to # ordinary SQL. # do_catchsql_test 14.3 { - SELECT sqlite_rename_column(0,0,0,0,0,0,0,0,0); + SELECT sqlite_rename_column(0,0,0,0,0,0,0,0,0,0); } {1 {no such function: sqlite_rename_column}} #------------------------------------------------------------------------- # reset_db ADDED test/altercol2.test Index: test/altercol2.test ================================================================== --- /dev/null +++ test/altercol2.test @@ -0,0 +1,49 @@ +# 2019 July 3 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#************************************************************************* +# This file implements regression tests for SQLite library. The +# focus of this script is testing that SQLite can handle a subtle +# file format change that may be used in the future to implement +# "ALTER TABLE ... RENAME COLUMN ... TO". +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix altercol2 + +# If SQLITE_OMIT_ALTERTABLE is defined, omit this file. +ifcapable !altertable { + finish_test + return +} + +sqlite3_db_config db DQS_DDL 1 + +#------------------------------------------------------------------------- +# Check that the ALTER TABLE statement used in bug [9b78184b] is now +# an error (because it effectively transforms a constant string into a +# column ref). +# +do_execsql_test 1.0 { + CREATE TABLE t0(c1, c2); + INSERT INTO t0(c1, c2) VALUES ('a', 1); + CREATE INDEX i0 ON t0("C3"); +} + +do_catchsql_test 1.1 { + ALTER TABLE t0 RENAME COLUMN c1 TO c3; +} {1 {schema contains ambiguous double-quoted string}} + +do_execsql_test 1.2 { + ALTER TABLE t0 RENAME COLUMN c1 TO c4; + SELECT DISTINCT * FROM t0; +} {a 1} + +finish_test