]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add experimental implementations of ALTER TABLE commands to add and drop CHECK and...
authordan <Dan Kennedy>
Sat, 27 Sep 2025 14:59:21 +0000 (14:59 +0000)
committerdan <Dan Kennedy>
Sat, 27 Sep 2025 14:59:21 +0000 (14:59 +0000)
FossilOrigin-Name: d939b25d76fe70a3255cfe38097d4489323028cd05e5512a98dce06b48eee445

manifest
manifest.tags
manifest.uuid
src/alter.c
src/parse.y
src/prepare.c
src/sqliteInt.h
test/altercons.test [new file with mode: 0644]

index 083b1bedf92a5005e58bc3b5c48e52ac79d208cd..d9014583b495ce0ff3ff53b59db3d48804c285f0 100644 (file)
--- 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.
index bec971799ff1b8ee641c166c7aeb22d12c785393..2d3555f577fc5ba7b3cfd08585e3f776c1e4787c 100644 (file)
@@ -1,2 +1,2 @@
-branch trunk
-tag trunk
+branch alter-table-constraints
+tag alter-table-constraints
index bf59fd1bbb5555e7d42af0e97926f1b61b439e55..8d93d0bad356ea8e07f31df7981ff5f0c1db28bb 100644 (file)
@@ -1 +1 @@
-b44650f907e9cb4ec908bb7525488e309946fac9d84cdac4cdde730527a440a9
+d939b25d76fe70a3255cfe38097d4489323028cd05e5512a98dce06b48eee445
index a7255e75ef2da1e5df0e1c5c6570e1b6c26b386b..23847470a49961905b8f1294bb1de2a1351ee767 100644 (file)
@@ -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));
 }
index 617eb7303b6ee2f2293544a5cace5e4d95f908be..bcc3e61b2a1a687740574abd1cf2b6932a24d1c6 100644 (file)
@@ -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.
index 539360b745173cd95d7aa3ce237f0309d459411b..be9e496f115177dc2ab052915aee687e3549846c 100644 (file)
@@ -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], 
index cf9294dccc663c7677772ba8470bc460cdbc25c6..81cfbf8ab3de0079556d6c74c4617954e80c6baa 100644 (file)
@@ -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 (file)
index 0000000..af70d0f
--- /dev/null
@@ -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<b) NOT NULL, b) }
+      { CREATE TABLE t1(a CHECK(a<b), b) }
+
+  6 a { CREATE TABLE t1(a CHECK(a<b) CONSTRAINT nn NOT NULL, b) }
+      { CREATE TABLE t1(a CHECK(a<b), b) }
+
+  7 b { CREATE TABLE t1(a, b NOT NULL PRIMARY KEY) }
+      { CREATE TABLE t1(a, b PRIMARY KEY) }
+} {
+  reset_db
+
+  do_execsql_test 3.$tn.0 $before
+
+  do_execsql_test 3.$tn.1 "
+    ALTER TABLE t1 ALTER COLUMN $col DROP NOT NULL
+  "
+
+  do_execsql_test 3.$tn.2 {
+    SELECT sql FROM sqlite_schema WHERE name='t1'
+  } [list [string trim $after]]
+}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+do_execsql_test 4.0 {
+  CREATE TABLE t2(x, y CONSTRAINT ccc UNIQUE);
+}
+do_execsql_test 4.1 {
+  ALTER TABLE t2 ALTER x DROP NOT NULL;
+  ALTER TABLE t2 ALTER x DROP NOT NULL;
+  ALTER TABLE t2 ALTER x DROP NOT NULL;
+} {}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+
+do_execsql_test 5.1 {
+  CREATE TABLE t3(a INTEGER PRIMARY KEY, b);
+  INSERT INTO t3 VALUES(1000, NULL);
+}
+
+do_catchsql_test 5.2.1 {
+  ALTER TABLE t3 ALTER b SET NOT NULL
+} {1 {constraint failed}}
+
+do_test 5.2.2 {
+  sqlite3_errcode db
+} {SQLITE_CONSTRAINT}
+
+foreach {tn before alter after} {
+  1  { CREATE TABLE t1(a, b) }
+     { ALTER TABLE t1 ALTER a SET NOT NULL }
+     { CREATE TABLE t1(a NOT NULL, b) }
+
+  2  { CREATE TABLE t1(a, b) }
+     { ALTER TABLE t1 ALTER a SET NOT NULL ON CONFLICT FAIL }
+     { CREATE TABLE t1(a NOT NULL ON CONFLICT FAIL, b) }
+
+  3  { CREATE TABLE t1(a, b) }
+     { ALTER TABLE t1 ALTER a SET NOT NULL ON CONFLICT fail; }
+     { CREATE TABLE t1(a NOT NULL ON CONFLICT fail, b) }
+
+  4  { CREATE TABLE t1(a, b) }
+     { ALTER TABLE t1 ALTER b SET NOT   NULL ON CONFLICT IGNORE ; }
+     { CREATE TABLE t1(a, b NOT   NULL ON CONFLICT IGNORE) }
+
+  5  { CREATE TABLE t1(a, 'a b c' VARCHAR(10), UNIQUE(a)) }
+     { ALTER TABLE t1 ALTER 'a b c' SET NOT NULL }
+     { CREATE TABLE t1(a, 'a b c' VARCHAR(10) NOT NULL, UNIQUE(a)) }
+} {
+  reset_db
+  do_execsql_test 5.3.$tn.1 $before
+  do_execsql_test 5.3.$tn.2 $alter
+  do_execsql_test 5.3.$tn.3 {
+    SELECT sql FROM sqlite_schema WHERE name='t1';
+  } [list [string trim $after]]
+}
+
+do_execsql_test 5.4.1 {
+  CREATE TABLE x1(a, b, c);
+}
+do_catchsql_test 5.4.2 {
+  ALTER TABLE x1 ALTER d SET NOT NULL;
+} {1 {no such column: d}}
+do_catchsql_test 5.4.3 {
+  ALTER TABLE x2 ALTER c SET NOT NULL;
+} {1 {no such table: x2}}
+do_catchsql_test 5.4.4 {
+  ALTER TABLE temp.x1 ALTER c SET NOT NULL;
+} {1 {no such table: temp.x1}}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+
+do_execsql_test 6.1 {
+  CREATE TABLE t1(a, b, c);
+  INSERT INTO t1 VALUES(1, 2, 3);
+  INSERT INTO t1 VALUES(4, 5, 6);
+}
+
+do_catchsql_test 6.2.1 {
+  ALTER TABLE t1 ADD CONSTRAINT nn CHECK (c!=6);
+} {1 {constraint failed}}
+do_execsql_test 6.2.2 {
+  DELETE FROM t1 WHERE c=6;
+  ALTER TABLE t1 ADD CONSTRAINT nn CHECK (c!=6);
+} {}
+do_catchsql_test 6.2.3 {
+  INSERT INTO t1 VALUES(4, 5, 6);
+} {1 {CHECK constraint failed: nn}}
+
+foreach {tn before alter after} {
+  1 { CREATE TABLE t1(a, b) }
+    { ALTER TABLE t1 ADD CONSTRAINT nn 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