From: dan Date: Sat, 27 Sep 2025 14:59:21 +0000 (+0000) Subject: Add experimental implementations of ALTER TABLE commands to add and drop CHECK and... X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=ba1ee803eede99b3eefae0dd6ec6d2dbce9817c6;p=thirdparty%2Fsqlite.git Add experimental implementations of ALTER TABLE commands to add and drop CHECK and NOT NULL constraints. FossilOrigin-Name: d939b25d76fe70a3255cfe38097d4489323028cd05e5512a98dce06b48eee445 --- diff --git a/manifest b/manifest index 083b1bedf9..d9014583b4 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C ext/wasm/c-pp.c:\sadd\s#savepoint\ssupport.\sConsolidate\show\sthe\sdiverse\ssqlite3_stmt\shandles\sare\smanaged. -D 2025-09-27T13:34:02.718 +C Add\sexperimental\simplementations\sof\sALTER\sTABLE\scommands\sto\sadd\sand\sdrop\sCHECK\sand\sNOT\sNULL\sconstraints. +D 2025-09-27T14:59:21.750 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -670,7 +670,7 @@ F mptest/multiwrite01.test dab5c5f8f9534971efce679152c5146da265222d F sqlite.pc.in 42b7bf0d02e08b9e77734a47798d1a55a9e0716b F sqlite3.1 1b9c24374a85dfc7eb8fa7c4266ee0db4f9609cceecfc5481cd8307e5af04366 F sqlite3.pc.in e6dee284fba59ef500092fdc1843df3be8433323a3733c91da96690a50a5b398 -F src/alter.c fc7bbbeb9e89c7124bf5772ce474b333b7bdc18d6e080763211a40fde69fb1da +F src/alter.c 8eeaacd19f6b5948600c7d567c1e22a906d60777c1de8be0d2caa6b68a058a3a F src/analyze.c 03bcfc083fc0cccaa9ded93604e1d4244ea245c17285d463ef6a60425fcb247d F src/attach.c 9af61b63b10ee702b1594ecd24fb8cea0839cfdb6addee52fba26fa879f5db9d F src/auth.c 54ab9c6c5803b47c0d45b76ce27eff22a03b4b1f767c5945a3a4eb13aa4c78dc @@ -726,12 +726,12 @@ F src/os_win.c f81a7cffdfe8c593a840895b3f64290714f0186b06302d2c397012252d830374 F src/os_win.h 4c247cdb6d407c75186c94a1e84d5a22cbae4adcec93fcae8d2bc1f956fd1f19 F src/pager.c 113f9149092ccff6cf90e97c2611200e5a237f13d26c394bc9fd933377852764 F src/pager.h 6137149346e6c8a3ddc1eeb40aee46381e9bc8b0fcc6dda8a1efde993c2275b8 -F src/parse.y 619c3e92a54686c5e47923688c4b9bf7ec534a4690db5677acc28b299c403250 +F src/parse.y 853bfe037292ab3731b0753a92291d248161e721dd6893affb9d181d65f43702 F src/pcache.c 588cc3c5ccaaadde689ed35ce5c5c891a1f7b1f4d1f56f6cf0143b74d8ee6484 F src/pcache.h 1497ce1b823cf00094bb0cf3bac37b345937e6f910890c626b16512316d3abf5 F src/pcache1.c 131ca0daf4e66b4608d2945ae76d6ed90de3f60539afbd5ef9ec65667a5f2fcd F src/pragma.c ecec75795c1821520266e4f93fa8840cce48979af532db06f085e36a7813860f -F src/prepare.c 2af0b5c1ec787c8eebd21baa9d79caf4a4dc3a18e76ce2edbf2027d706bca37a +F src/prepare.c f6a6e28a281bd1d1da12f47d370a81af46159b40f73bf7fa0b276b664f9c8b7d F src/printf.c 5f0c957af9699e849d786e8fbaa3baab648ca5612230dc17916434c14bc8698f F src/random.c 606b00941a1d7dd09c381d3279a058d771f406c5213c9932bbd93d5587be4b9c F src/resolve.c f8d1d011aba0964ff1bdccd049d4d2c2fec217efd90d202a4bb775e926b2c25d @@ -741,7 +741,7 @@ F src/shell.c.in 0ab96b4d875ec03e1b5d0c97aa3f2f68d3b6c926f270488e26f3088d1755481 F src/sqlite.h.in 5732519a2acb09066032ceac21f25996eb3f28f807a4468e30633c7c70faae1c F src/sqlite3.rc 015537e6ac1eec6c7050e17b616c2ffe6f70fca241835a84a4f0d5937383c479 F src/sqlite3ext.h 3f0c4ed6934e7309a61c6f3c30f70a30a5b869f785bb3d9f721a36c5e4359126 -F src/sqliteInt.h 673c7c5d7e77552452b21499717233329809f252458f77a798977737f51b31b8 +F src/sqliteInt.h 9e65623e73c1c6d45e8bba3bad2d86d90cc289b2923ca35a26db5328d9f1ed60 F src/sqliteLimit.h fe70bd8983e5d317a264f2ea97473b359faf3ebb0827877a76813f5cf0cdc364 F src/status.c 0e72e4f6be6ccfde2488eb63210297e75f569f3ce9920f6c3d77590ec6ce5ffd F src/table.c 0f141b58a16de7e2fbe81c308379e7279f4c6b50eb08efeec5892794a0ba30d1 @@ -839,6 +839,7 @@ F test/alter4.test 37cafe164067a6590a0ee4cec780bddbbaa33dc50b11542dcfbe0e6562649 F test/alterauth.test 63442ba61ceb0c1eeb63aac1f4f5cebfa509d352276059d27106ae256bafc959 F test/alterauth2.test 48967abae0494d9a300d1c92473d99fcb66edfcc23579c89322f033f49410adc F test/altercol.test b43fb5725332f4cf8cff0280605202c1672e808281accea60a066d2ccc5129e5 +F test/altercons.test 2e05836679a139317b8ac18c48c86d814b38011a7f225cbd52880fc8bc22c2f5 F test/altercorrupt.test 2e1d705342cf9d7de884518ddbb053fd52d7e60d2b8869b7b63b2fda68435c12 F test/alterdropcol.test a653a3945f964d26845ec0cd0a8e74189f46de3119a984c5bc45457da392612e F test/alterdropcol2.test 527fce683b200d620f560f666c44ae33e22728e990a10a48a543280dfd4b4d41 @@ -2169,8 +2170,11 @@ F tool/version-info.c 3b36468a90faf1bbd59c65fd0eb66522d9f941eedd364fabccd7227350 F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7 F tool/warnings.sh 1ad0169b022b280bcaaf94a7fa231591be96b514230ab5c98fbf15cd7df842dd F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f -P b05c47009120840f74955114082f3a9c1206a81bd935a503fc359b5bde61c996 -R 5c9bc3e1c5ff8428ad8c5e411548be45 -U stephan -Z 0c4774a9e741ee60e5e425d698f46673 +P b44650f907e9cb4ec908bb7525488e309946fac9d84cdac4cdde730527a440a9 +R 62bf02b8a31c34d045e943408d7ca34a +T *branch * alter-table-constraints +T *sym-alter-table-constraints * +T -sym-trunk * +U dan +Z df1bd70c76c4689d412ec3f686f545c6 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.tags b/manifest.tags index bec971799f..2d3555f577 100644 --- a/manifest.tags +++ b/manifest.tags @@ -1,2 +1,2 @@ -branch trunk -tag trunk +branch alter-table-constraints +tag alter-table-constraints diff --git a/manifest.uuid b/manifest.uuid index bf59fd1bbb..8d93d0bad3 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -b44650f907e9cb4ec908bb7525488e309946fac9d84cdac4cdde730527a440a9 +d939b25d76fe70a3255cfe38097d4489323028cd05e5512a98dce06b48eee445 diff --git a/src/alter.c b/src/alter.c index a7255e75ef..23847470a4 100644 --- a/src/alter.c +++ b/src/alter.c @@ -1049,6 +1049,25 @@ static RenameToken *renameColumnTokenNext(RenameCtx *pCtx){ return pBest; } +/* +** Set the error message of the context passed as the first argument to +** the result of formatting zFmt using printf() style formatting. +*/ +static void errorMPrintf(sqlite3_context *pCtx, const char *zFmt, ...){ + sqlite3 *db = sqlite3_context_db_handle(pCtx); + char *zErr = 0; + va_list ap; + va_start(ap, zFmt); + zErr = sqlite3VMPrintf(db, zFmt, ap); + va_end(ap); + if( zErr ){ + sqlite3_result_error(pCtx, zErr, -1); + sqlite3DbFree(db, zErr); + }else{ + sqlite3_result_error_nomem(pCtx); + } +} + /* ** An error occurred while parsing or otherwise processing a database ** object (either pParse->pNewTable, pNewIndex or pNewTrigger) as part of an @@ -2313,6 +2332,582 @@ exit_drop_column: sqlite3SrcListDelete(db, pSrc); } +/* +** Return the number of bytes of leading whitespace in string z. +*/ +static int getWhitespace(const u8 *z){ + int nRet = 0; + while( 1 ){ + int t = 0; + int n = sqlite3GetToken(&z[nRet], &t); + if( t!=TK_SPACE ) break; + nRet += n; + } + return nRet; +} + + +/* +** Skip over any leading whitespace, then read the first token from the +** string passed as the first argument. Set *piToken to the type of the +** token before returning the total number of bytes consumed (including +** any whitespace). +*/ +static int getConstraintToken(const u8 *z, int *piToken){ + int iOff = 0; + int t = 0; + do { + iOff += sqlite3GetToken(&z[iOff], &t); + }while( t==TK_SPACE ); + + *piToken = t; + + if( t==TK_LP ){ + int nNest = 1; + while( nNest>0 ){ + iOff += sqlite3GetToken(&z[iOff], &t); + if( t==TK_LP ){ + nNest++; + }else if( t==TK_RP ){ + t = TK_LP; + nNest--; + }else if( t==TK_ILLEGAL ){ + break; + } + } + } + + *piToken = t; + return iOff; +} + +/* +** Argument z points into the body of a constraint - specifically the +** token following the first of the +** of bytes in string z to the end of the current constraint. +*/ +static int getConstraint(const u8 *z){ + int iOff = 0; + int t = 0; + + /* Now, the current constraint proceeds until the next occurence of one + ** of the following tokens: + ** + ** CONSTRAINT, PRIMARY, NOT, UNIQUE, CHECK, DEFAULT, + ** COLLATE, REFERENCES, RP, COMMA + ** + ** Also exit the loop if ILLEGAL turns up. + */ + while( 1 ){ + int n = getConstraintToken(&z[iOff], &t); + if( t==TK_CONSTRAINT || t==TK_PRIMARY || t==TK_NOT || t==TK_UNIQUE + || t==TK_CHECK || t==TK_DEFAULT || t==TK_COLLATE || t==TK_REFERENCES + || t==TK_RP || t==TK_COMMA || t==TK_ILLEGAL + ){ + break; + } + iOff += n; + } + + return iOff; +} + +static int quotedCompare( + const u8 *zQuote, + int nQuote, + const u8 *zCmp, + int *pRes +){ + char *zCopy = 0; + + zCopy = sqlite3MallocZero(nQuote+1); + if( zCopy==0 ){ + return SQLITE_NOMEM_BKPT; + } + memcpy(zCopy, zQuote, nQuote); + sqlite3Dequote(zCopy); + *pRes = sqlite3_stricmp((const char*)zCopy, (const char*)zCmp); + sqlite3_free(zCopy); + return SQLITE_OK; +} + +/* +** The second argument is passed a CREATE TABLE statement. This function +** attempts to find the offset of the first token of the first column +** definition in the table. If successful, it sets (*piOff) to the offset +** and return SQLITE_OK. Otherwise, if an error occurs, it leaves an +** error code and message in the context handle passed as the first argument +** and returns SQLITE_ERROR. +*/ +static int skipCreateTable(sqlite3_context *ctx, const u8 *zSql, int *piOff){ + int iOff = 0; + + /* Jump past the "CREATE TABLE" bit. */ + while( 1 ){ + int t = 0; + iOff += sqlite3GetToken(&zSql[iOff], &t); + if( t==TK_LP ) break; + if( t==TK_ILLEGAL ){ + sqlite3_result_error_code(ctx, SQLITE_CORRUPT_BKPT); + return SQLITE_ERROR; + } + } + + *piOff = iOff; + return SQLITE_OK; +} + +/* +** The implementation of internal function sqlite3_drop_constraint(). +*/ +static void dropConstraintFunc( + sqlite3_context *ctx, + int NotUsed, + sqlite3_value **argv +){ + const char *zSpace = " "; + const u8 *zSql = sqlite3_value_text(argv[0]); + const u8 *zCons = 0; + int iNotNull = -1; + int ii; + int iOff = 0; + int iStart = 0; + int iEnd = 0; + char *zNew = 0; + int t = 0; + + /* Jump past the "CREATE TABLE" bit. */ + if( skipCreateTable(ctx, zSql, &iOff) ) return; + + if( sqlite3_value_type(argv[1])==SQLITE_INTEGER ){ + iNotNull = sqlite3_value_int(argv[1]); + }else{ + zCons = sqlite3_value_text(argv[1]); + } + + /* Search for the named constraint within column definitions. */ + for(ii=0; iEnd==0; ii++){ + + /* Now parse the column or table constraint definition. Search + ** for the token CONSTRAINT if this is a DROP CONSTRAINT command, or + ** NOT in the right column if this is a DROP NOT NULL. */ + iStart = iOff - 1; + while( 1 ){ + iOff += getConstraintToken(&zSql[iOff], &t); + if( t==TK_CONSTRAINT && (zCons || iNotNull==ii) ){ + /* Check if this is the constraint we are searching for. */ + int nTok = 0; + int cmp = 1; + + /* Skip past any whitespace. */ + iOff += getWhitespace(&zSql[iOff]); + + /* Compare the next token - which may be quoted - with the name of + ** the constraint being dropped. */ + nTok = getConstraintToken(&zSql[iOff], &t); + if( zCons ){ + if( quotedCompare(&zSql[iOff], nTok, zCons, &cmp) ) return; + } + iOff += nTok; + + /* The first token of the constraint definition. */ + iOff += getConstraintToken(&zSql[iOff], &t); + iOff += getConstraint(&zSql[iOff]); + + if( cmp==0 || (iNotNull>=0 && t==TK_NOT) ){ + if( t!=TK_NOT && t!=TK_CHECK ){ + errorMPrintf(ctx, "constraint may not be dropped: %s", zCons); + return; + } + iEnd = iOff; + break; + } + + }else if( t==TK_NOT && iNotNull==ii ){ + iEnd = iOff + getConstraint(&zSql[iOff]); + break; + }else if( t==TK_RP || t==TK_ILLEGAL ){ + iEnd = -1; + break; + }else if( t==TK_COMMA ){ + break; + } + iStart = iOff; + } + } + + /* If the constraint has not been found it is an error. */ + if( iEnd<=0 ){ + if( zCons ){ + errorMPrintf(ctx, "no such constraint: %s", zCons); + }else{ + /* SQLite follows postgres in that a DROP NOT NULL on a column that is + ** not NOT NULL is not an error. So just return the original SQL here. */ + sqlite3_result_text(ctx, (const char*)zSql, -1, SQLITE_TRANSIENT); + } + }else{ + /* Figure out if an extra space is required following the constraint. */ + sqlite3GetToken(&zSql[iEnd], &t); + if( t==TK_RP || t==TK_SPACE || t==TK_COMMA ){ + zSpace = ""; + } + + zNew = sqlite3_mprintf("%.*s%s%s", iStart, zSql, zSpace, &zSql[iEnd]); + if( zNew==0 ){ + sqlite3_result_error_nomem(ctx); + }else{ + sqlite3_result_text(ctx, zNew, -1, SQLITE_TRANSIENT); + sqlite3_free(zNew); + } + } +} + +/* +** Implementation of internal SQL function: +** +** sqlite_add_constraint(SQL, CONSTRAINT-TEXT, ICOL) +*/ +static void addConstraintFunc( + sqlite3_context *ctx, + int NotUsed, + sqlite3_value **argv +){ + const u8 *zSql = sqlite3_value_text(argv[0]); + const char *zCons = (const char*)sqlite3_value_text(argv[1]); + int iCol = sqlite3_value_int(argv[2]); + int iOff = 0; + int ii; + char *zNew = 0; + int t = 0; + + if( skipCreateTable(ctx, zSql, &iOff) ) return; + + for(ii=0; ii<=iCol || (iCol<0 && t!=TK_RP); ii++){ + iOff += getConstraintToken(&zSql[iOff], &t); + while( 1 ){ + int nTok = getConstraintToken(&zSql[iOff], &t); + if( t==TK_COMMA || t==TK_RP ) break; + if( t==TK_ILLEGAL ){ + sqlite3_result_error_code(ctx, SQLITE_CORRUPT_BKPT); + return; + } + iOff += nTok; + } + } + + iOff += getWhitespace(&zSql[iOff]); + + if( iCol<0 ){ + zNew = sqlite3_mprintf("%.*s, %s%s", iOff, zSql, zCons, &zSql[iOff]); + }else{ + zNew = sqlite3_mprintf("%.*s %s%s", iOff, zSql, zCons, &zSql[iOff]); + } + + if( zNew==0 ){ + sqlite3_result_error_nomem(ctx); + }else{ + sqlite3_result_text(ctx, zNew, -1, SQLITE_TRANSIENT); + sqlite3_free(zNew); + } +} + +/* +** Find a column named pCol in table pTab. If successful, set output +** parameter *piCol to the index of the column in the table and return +** SQLITE_OK. Otherwise, set *piCol to -1 and return an SQLite error +** code. +*/ +static int alterFindCol(Parse *pParse, Table *pTab, Token *pCol, int *piCol){ + sqlite3 *db = pParse->db; + char *zName = sqlite3NameFromToken(db, pCol); + int rc = SQLITE_NOMEM; + int iCol = -1; + + if( zName ){ + iCol = sqlite3ColumnIndex(pTab, zName); + if( iCol<0 ){ + sqlite3ErrorMsg(pParse, "no such column: %s", zName); + rc = SQLITE_ERROR; + }else{ + rc = SQLITE_OK; + } + } + + sqlite3DbFree(db, zName); + *piCol = iCol; + return rc; +} + +void alterDropConstraint( + Parse *pParse, + SrcList *pSrc, + Token *pCons, + Token *pCol +){ + sqlite3 *db = pParse->db; + Table *pTab = 0; + int iDb = 0; + const char *zDb = 0; + char *zArg = 0; + + /* Look up the table being altered. */ + assert( sqlite3BtreeHoldsAllMutexes(db) ); + pTab = sqlite3LocateTableItem(pParse, 0, &pSrc->a[0]); + if( !pTab ) goto exit_drop_cons; + + /* Make sure this is not an attempt to ALTER a view, virtual table or + ** system table. */ + if( SQLITE_OK!=isAlterableTable(pParse, pTab) ) goto exit_drop_cons; + if( SQLITE_OK!=isRealTable(pParse, pTab, 1) ) goto exit_drop_cons; + + /* Edit the sqlite_schema table */ + iDb = sqlite3SchemaToIndex(db, pTab->pSchema); + assert( iDb>=0 ); + zDb = db->aDb[iDb].zDbSName; + + if( pCons ){ + zArg = sqlite3MPrintf(db, "%.*Q", pCons->n, pCons->z); + }else{ + int iCol; + if( alterFindCol(pParse, pTab, pCol, &iCol) ) goto exit_drop_cons; + zArg = sqlite3MPrintf(db, "%d", iCol); + } + + /* Edit the SQL for the named table. */ + sqlite3NestedParse(pParse, + "UPDATE \"%w\"." LEGACY_SCHEMA_TABLE " SET " + "sql = sqlite_drop_constraint(sql, %s) " + "WHERE type='table' AND tbl_name=%Q COLLATE nocase" + , zDb, zArg, pTab->zName + ); + sqlite3DbFree(db, zArg); + + /* Finally, reload the database schema. */ + renameReloadSchema(pParse, iDb, INITFLAG_AlterDropCons); + + exit_drop_cons: + sqlite3SrcListDelete(db, pSrc); +} + + +/* +** Prepare a statement of the form: +** +** ALTER TABLE pSrc DROP CONSTRAINT pCons +*/ +void sqlite3AlterDropConstraint(Parse *pParse, SrcList *pSrc, Token *pCons){ + return alterDropConstraint(pParse, pSrc, pCons, 0); +} + +/* +** Prepare a statement of the form: +** +** ALTER TABLE pSrc ALTER pCol DROP NOT NULL +*/ +void sqlite3AlterDropNotNull(Parse *pParse, SrcList *pSrc, Token *pCol){ + return alterDropConstraint(pParse, pSrc, 0, pCol); +} + +/* +** The implementation of SQL function sqlite_fail(MSG). This takes a single +** argument, and returns it as an error message with the error code set to +** SQLITE_CONSTRAINT. +*/ +static void failConstraintFunc( + sqlite3_context *ctx, + int NotUsed, + sqlite3_value **argv +){ + const char *zText = (const char*)sqlite3_value_text(argv[0]); + int err = sqlite3_value_int(argv[1]); + sqlite3_result_error(ctx, zText, -1); + sqlite3_result_error_code(ctx, err); +} + +/* +** Find the table named by the first entry in source list pSrc. If successful, +** return a pointer to the Table structure and set output variable (*pzDb) +** to point to the name of the database containin the table (i.e. "main", +** "temp" or the name of an attached database). +** +** If the table cannot be located, return NULL. The value of the two output +** parameters is undefined in this case. +*/ +static Table *alterFindTable( + Parse *pParse, + SrcList *pSrc, + int *piDb, + const char **pzDb +){ + sqlite3 *db = pParse->db; + Table *pTab = 0; + assert( sqlite3BtreeHoldsAllMutexes(db) ); + pTab = sqlite3LocateTableItem(pParse, 0, &pSrc->a[0]); + sqlite3SrcListDelete(db, pSrc); + if( pTab ){ + int iDb = sqlite3SchemaToIndex(db, pTab->pSchema); + *pzDb = db->aDb[iDb].zDbSName; + *piDb = iDb; + } + return pTab; +} + +/* +** Prepare a statement of the form: +** +** ALTER TABLE pSrc ALTER pCol SET NOT NULL +*/ +void sqlite3AlterSetNotNull( + Parse *pParse, + SrcList *pSrc, + Token *pCol +){ + Table *pTab = 0; + int iCol = 0; + int iDb = 0; + const char *zDb = 0; + const char *pCons = 0; + int nCons = 0; + int t = 0; + + /* Look up the table being altered. */ + pTab = alterFindTable(pParse, pSrc, &iDb, &zDb); + if( !pTab ) return; + + /* Find the column being altered. */ + if( alterFindCol(pParse, pTab, pCol, &iCol) ){ + return; + } + + /* Find the start of the constraint definition */ + pCons = &pCol->z[pCol->n]; + pCons += getConstraintToken((const u8*)pCons, &t); + pCons += getWhitespace((const u8*)pCons); + + /* Find the length in bytes of the constraint definition */ + nCons = pParse->sLastToken.z - pCons; + if( pCons[nCons-1]==';' ) nCons--; + while( sqlite3Isspace(pCons[nCons-1]) ) nCons--; + + /* Search for a constraint violation. Throw an exception if one is found. */ + sqlite3NestedParse(pParse, + "SELECT sqlite_fail('constraint failed', %d) " + "FROM %Q.%Q AS x WHERE x.%.*s IS NULL", + SQLITE_CONSTRAINT, zDb, pTab->zName, (int)pCol->n, pCol->z + ); + + /* Edit the SQL for the named table. */ + sqlite3NestedParse(pParse, + "UPDATE \"%w\"." LEGACY_SCHEMA_TABLE " SET " + "sql = sqlite_add_constraint(sqlite_drop_constraint(sql, %d), %.*Q, %d) " + "WHERE type='table' AND tbl_name=%Q COLLATE nocase" + , zDb, iCol, nCons, pCons, iCol, pTab->zName + ); + + /* Finally, reload the database schema. */ + renameReloadSchema(pParse, iDb, INITFLAG_AlterDropCons); +} + +/* +** Implementation of internal SQL function: +** +** sqlite_find_constraint(SQL, CONSTRAINT-NAME) +** +** This function returns true if the SQL passed as the first argument is a +** CREATE TABLE that contains a constraint with the name CONSTRAINT-NAME, +** or false otherwise. +*/ +static void findConstraintFunc( + sqlite3_context *ctx, + int NotUsed, + sqlite3_value **argv +){ + const u8 *zSql = 0; + const u8 *zCons = 0; + int iOff = 0; + int t = 0; + + zSql = sqlite3_value_text(argv[0]); + zCons = sqlite3_value_text(argv[1]); + + while( t!=TK_LP && t!=TK_ILLEGAL ){ + iOff += sqlite3GetToken(&zSql[iOff], &t); + } + + while( 1 ){ + iOff += getConstraintToken(&zSql[iOff], &t); + if( t==TK_CONSTRAINT ){ + int nTok = 0; + int cmp = 0; + iOff += getWhitespace(&zSql[iOff]); + nTok = getConstraintToken(&zSql[iOff], &t); + if( quotedCompare(&zSql[iOff], nTok, zCons, &cmp) ) return; + if( cmp==0 ){ + sqlite3_result_int(ctx, 1); + return; + } + }else if( t==TK_ILLEGAL ){ + break; + } + } + + sqlite3_result_int(ctx, 0); +} + +void sqlite3AlterAddConstraint( + Parse *pParse, /* Parse context */ + SrcList *pSrc, /* Table to add constraint to */ + Token *pFirst, /* First token of new constraint */ + Token *pCons, /* Name of new constraint */ + const char *pExpr, /* Text of CHECK expression */ + int nExpr /* Size of pExpr in bytes */ +){ + Table *pTab = 0; + int iDb = 0; + const char *zDb = 0; + int nCons; + + /* Look up the table being altered. */ + pTab = alterFindTable(pParse, pSrc, &iDb, &zDb); + if( !pTab ) return; + + /* If this new constraint has a name, check that it is not a duplicate of + ** an existing constraint. It is an error if it is. */ + if( pCons ){ + char *zName = sqlite3NameFromToken(pParse->db, pCons); + + sqlite3NestedParse(pParse, + "SELECT sqlite_fail('constraint '||%Q||' already exists', %d) " + "FROM \"%w\"." LEGACY_SCHEMA_TABLE " " + "WHERE type='table' AND tbl_name=%Q COLLATE nocase " + "AND sqlite_find_constraint(sql, %Q)", + zName, SQLITE_ERROR, zDb, pTab->zName, zName + ); + sqlite3DbFree(pParse->db, zName); + } + + /* Search for a constraint violation. Throw an exception if one is found. */ + sqlite3NestedParse(pParse, + "SELECT sqlite_fail('constraint failed', %d) " + "FROM %Q.%Q WHERE (%.*s) IS NOT TRUE", + SQLITE_CONSTRAINT, zDb, pTab->zName, nExpr, pExpr + ); + + /* Edit the SQL for the named table. */ + nCons = pParse->sLastToken.z - pFirst->z; + if( pFirst->z[nCons-1]==';' ) nCons--; + while( sqlite3Isspace(pFirst->z[nCons-1]) ) nCons--; + sqlite3NestedParse(pParse, + "UPDATE \"%w\"." LEGACY_SCHEMA_TABLE " SET " + "sql = sqlite_add_constraint(sql, %.*Q, -1) " + "WHERE type='table' AND tbl_name=%Q COLLATE nocase" + , zDb, nCons, pFirst->z, pTab->zName + ); + + /* Finally, reload the database schema. */ + renameReloadSchema(pParse, iDb, INITFLAG_AlterDropCons); +} + /* ** Register built-in functions used to help implement ALTER TABLE */ @@ -2323,6 +2918,10 @@ void sqlite3AlterFunctions(void){ INTERNAL_FUNCTION(sqlite_rename_test, 7, renameTableTest), INTERNAL_FUNCTION(sqlite_drop_column, 3, dropColumnFunc), INTERNAL_FUNCTION(sqlite_rename_quotefix,2, renameQuotefixFunc), + INTERNAL_FUNCTION(sqlite_drop_constraint,2, dropConstraintFunc), + INTERNAL_FUNCTION(sqlite_fail, 2, failConstraintFunc), + INTERNAL_FUNCTION(sqlite_add_constraint, 3, addConstraintFunc), + INTERNAL_FUNCTION(sqlite_find_constraint,2, findConstraintFunc), }; sqlite3InsertBuiltinFuncs(aAlterTableFuncs, ArraySize(aAlterTableFuncs)); } diff --git a/src/parse.y b/src/parse.y index 617eb7303b..bcc3e61b2a 100644 --- a/src/parse.y +++ b/src/parse.y @@ -1831,22 +1831,41 @@ cmd ::= ANALYZE nm(X) dbnm(Y). {sqlite3Analyze(pParse, &X, &Y);} cmd ::= ALTER TABLE fullname(X) RENAME TO nm(Z). { sqlite3AlterRenameTable(pParse,X,&Z); } -cmd ::= ALTER TABLE add_column_fullname - ADD kwcolumn_opt columnname(Y) carglist. { + +cmd ::= alter_add(Y) carglist. { Y.n = (int)(pParse->sLastToken.z-Y.z) + pParse->sLastToken.n; sqlite3AlterFinishAddColumn(pParse, &Y); } -cmd ::= ALTER TABLE fullname(X) DROP kwcolumn_opt nm(Y). { - sqlite3AlterDropColumn(pParse, X, &Y); -} -add_column_fullname ::= fullname(X). { +alter_add(A) ::= ALTER TABLE fullname(X) ADD kwcolumn_opt nm(Y) typetoken(Z). { disableLookaside(pParse); sqlite3AlterBeginAddColumn(pParse, X); + sqlite3AddColumn(pParse, Y, Z); + A = Y; +} + +cmd ::= ALTER TABLE fullname(X) DROP kwcolumn_opt nm(Y). { + sqlite3AlterDropColumn(pParse, X, &Y); } cmd ::= ALTER TABLE fullname(X) RENAME kwcolumn_opt nm(Y) TO nm(Z). { sqlite3AlterRenameColumn(pParse, X, &Y, &Z); } +cmd ::= ALTER TABLE fullname(X) DROP CONSTRAINT nm(Y). { + sqlite3AlterDropConstraint(pParse, X, &Y); +} +cmd ::= ALTER TABLE fullname(X) ALTER kwcolumn_opt nm(Y) DROP NOT NULL. { + sqlite3AlterDropNotNull(pParse, X, &Y); +} +cmd ::= ALTER TABLE fullname(X) ALTER kwcolumn_opt nm(Y) SET NOT NULL onconf. { + sqlite3AlterSetNotNull(pParse, X, &Y); +} + +cmd ::= ALTER TABLE fullname(X) ADD CONSTRAINT(Y) nm(Z) CHECK LP(A) expr RP(B) onconf. { + sqlite3AlterAddConstraint(pParse, X, &Y, &Z, A.z+1, (B.z-A.z-1)); +} +cmd ::= ALTER TABLE fullname(X) ADD CHECK(Y) LP(A) expr RP(B) onconf. { + sqlite3AlterAddConstraint(pParse, X, &Y, 0, A.z+1, (B.z-A.z-1)); +} kwcolumn_opt ::= . kwcolumn_opt ::= COLUMNKW. diff --git a/src/prepare.c b/src/prepare.c index 539360b745..be9e496f11 100644 --- a/src/prepare.c +++ b/src/prepare.c @@ -33,7 +33,8 @@ static void corruptSchema( static const char *azAlterType[] = { "rename", "drop column", - "add column" + "add column", + "drop constraint" }; *pData->pzErrMsg = sqlite3MPrintf(db, "error in %s %s after %s: %s", azObj[0], azObj[1], diff --git a/src/sqliteInt.h b/src/sqliteInt.h index cf9294dccc..81cfbf8ab3 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -4267,10 +4267,11 @@ typedef struct { /* ** Allowed values for mInitFlags */ -#define INITFLAG_AlterMask 0x0003 /* Types of ALTER */ +#define INITFLAG_AlterMask 0x0007 /* Types of ALTER */ #define INITFLAG_AlterRename 0x0001 /* Reparse after a RENAME */ #define INITFLAG_AlterDrop 0x0002 /* Reparse after a DROP COLUMN */ #define INITFLAG_AlterAdd 0x0003 /* Reparse after an ADD COLUMN */ +#define INITFLAG_AlterDropCons 0x0004 /* Reparse after an ADD COLUMN */ /* Tuning parameters are set using SQLITE_TESTCTRL_TUNE and are controlled ** on debug-builds of the CLI using ".testctrl tune ID VALUE". Tuning @@ -5447,6 +5448,10 @@ void sqlite3Reindex(Parse*, Token*, Token*); void sqlite3AlterFunctions(void); void sqlite3AlterRenameTable(Parse*, SrcList*, Token*); void sqlite3AlterRenameColumn(Parse*, SrcList*, Token*, Token*); +void sqlite3AlterDropConstraint(Parse*, SrcList*, Token*); +void sqlite3AlterDropNotNull(Parse*, SrcList*, Token*); +void sqlite3AlterAddConstraint(Parse*,SrcList*,Token*,Token*,const char*,int); +void sqlite3AlterSetNotNull(Parse*, SrcList*, Token*); int sqlite3GetToken(const unsigned char *, int *); void sqlite3NestedParse(Parse*, const char*, ...); void sqlite3ExpirePreparedStatements(sqlite3*, int); diff --git a/test/altercons.test b/test/altercons.test new file mode 100644 index 0000000000..af70d0f562 --- /dev/null +++ b/test/altercons.test @@ -0,0 +1,231 @@ +# 2025 September 18 +# +# 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. +# +#************************************************************************* +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix altercons + +# If SQLITE_OMIT_ALTERTABLE is defined, omit this file. +ifcapable !altertable { + finish_test + return +} + +foreach {tn before after} { + 1 { CREATE TABLE t1(a, b CONSTRAINT abc CHECK(t1.a != t1.b)) } + { CREATE TABLE t1(a, b) } + + 2 { CREATE TABLE t1(a, b CONSTRAINT abc CHECK(t1.a != t1.b) NOT NULL) } + { CREATE TABLE t1(a, b NOT NULL) } + + 3 { CREATE TABLE t1(a, b CONSTRAINT abc CHECK(t1.a != t1.b)NOT NULL) } + { CREATE TABLE t1(a, b NOT NULL) } + + 3 { CREATE TABLE t1(a, b NOT NULL CONSTRAINT abc CHECK(t1.a != t1.b)); } + { CREATE TABLE t1(a, b NOT NULL) } + + 4 { CREATE TABLE t1(a, b, CONSTRAINT abc CHECK(t1.a != t1.b)) } + { CREATE TABLE t1(a, b) } + + 5 { CREATE TABLE t1(a, b, CONSTRAINT abc CHECK(t1.a != t1.b), PRIMARY KEY(a))} + { CREATE TABLE t1(a, b, PRIMARY KEY(a)) } + + 6 { CREATE TABLE t1(a, b,CONSTRAINT abc CHECK(t1.a != t1.b),PRIMARY KEY(a))} + { CREATE TABLE t1(a, b,PRIMARY KEY(a)) } + +} { + reset_db + + do_execsql_test 1.$tn.0 $before + + do_execsql_test 1.$tn.1 { + ALTER TABLE t1 DROP CONSTRAINT abc; + } {} + + do_execsql_test 1.$tn.2 { + SELECT sql FROM sqlite_schema WHERE name='t1' + } [list [string trim $after]] +} + +#------------------------------------------------------------------------- + +do_execsql_test 2.0 { + CREATE TABLE t2(x, y CONSTRAINT ccc UNIQUE); +} +do_catchsql_test 2.1 { + ALTER TABLE t2 DROP CONSTRAINT ccc +} {1 {constraint may not be dropped: ccc}} +do_catchsql_test 2.2 { + ALTER TABLE t2 DROP CONSTRAINT ddd +} {1 {no such constraint: ddd}} + +#------------------------------------------------------------------------- +reset_db +foreach {tn col before after} { + 1 a { CREATE TABLE t1(a NOT NULL, b) } + { CREATE TABLE t1(a, b) } + + 2 a { CREATE TABLE t1(a NOT NULL ON CONFLICT FAIL, b) } + { CREATE TABLE t1(a, b) } + + 3 a { CREATE TABLE t1(a NOT NULL ON CONFLICT FAIL UNIQUE, b) } + { CREATE TABLE t1(a UNIQUE, b) } + + 4 b { CREATE TABLE t1(a NOT NULL ON CONFLICT FAIL UNIQUE, b) } + { CREATE TABLE t1(a NOT NULL ON CONFLICT FAIL UNIQUE, b) } + + 5 a { CREATE TABLE t1(a CHECK(a=0) } + { CREATE TABLE t1(a, b, CONSTRAINT nn CHECK (a>=0)) } + + 2 { CREATE TABLE t1(a, b ) } + { ALTER TABLE t1 ADD CONSTRAINT nn CHECK (a>=0) } + { CREATE TABLE t1(a, b , CONSTRAINT nn CHECK (a>=0)) } + + 3 { CREATE TABLE t1(a, b ) } + { ALTER TABLE t1 ADD CHECK (a>=0) } + { CREATE TABLE t1(a, b , CHECK (a>=0)) } +} { + reset_db + do_execsql_test 6.3.$tn.1 $before + do_execsql_test 6.3.$tn.2 $alter + do_execsql_test 6.3.$tn.3 { + SELECT sql FROM sqlite_schema WHERE type='table'; + } [list [string trim $after]] +} + +do_execsql_test 6.4.1 { + CREATE TABLE b1(a, b, CONSTRAINT abc CHECK (a!=2)); +} +do_catchsql_test 6.4.2 { + ALTER TABLE b1 ADD CONSTRAINT abc CHECK (a!=3); +} {1 {constraint abc already exists}} +do_execsql_test 6.4.1 { + SELECT sql FROM sqlite_schema WHERE tbl_name='b1' +} {{CREATE TABLE b1(a, b, CONSTRAINT abc CHECK (a!=2))}} + +finish_test