#include "sqliteInt.h" #include "unity.h" #include #include /* Global db handle for tests */ static sqlite3 *gDb = NULL; /* Helpers to create minimal Parse/pNewTable environment needed by sqlite3AlterFinishAddColumn */ static void init_db_and_base_table(const char *zCreate){ int rc; rc = sqlite3_open(":memory:", &gDb); TEST_ASSERT_EQUAL_INT(SQLITE_OK, rc); rc = sqlite3_exec(gDb, zCreate, 0, 0, 0); TEST_ASSERT_EQUAL_INT(SQLITE_OK, rc); } /* Allocate a minimal Table used as pParse->pNewTable for ALTER ADD COLUMN. */ static Table *allocNewAlterTable(sqlite3 *db, const char *zTabName){ Table *pNew = (Table*)sqlite3DbMallocZero(db, sizeof(Table)); TEST_ASSERT_NOT_NULL(pNew); /* Ordinary table with schema = main */ pNew->pSchema = db->aDb[0].pSchema; /* pNew->zName must be "sqlite_altertab_" as code expects to skip first 16 chars */ pNew->zName = sqlite3MPrintf(db, "sqlite_altertab_%s", zTabName); TEST_ASSERT_NOT_NULL(pNew->zName); /* Single column which is the new column */ pNew->nCol = 1; pNew->aCol = (Column*)sqlite3DbMallocZero(db, sizeof(Column)); TEST_ASSERT_NOT_NULL(pNew->aCol); /* Make sure this is treated as ordinary table */ pNew->tabFlags = 0; /* IsOrdinaryTable() should pass */ /* Some reasonable addColOffset value used by printf() in nested parse */ pNew->u.tab.addColOffset = 1; return pNew; } /* Construct a TK_SPAN Expr whose pLeft has the given op (e.g., TK_NULL, TK_INTEGER, TK_FUNCTION) */ static Expr *makeSpanExprWithLeft(sqlite3 *db, int leftOp){ Expr *pLeft = (Expr*)sqlite3DbMallocZero(db, sizeof(Expr)); TEST_ASSERT_NOT_NULL(pLeft); pLeft->op = (u8)leftOp; Expr *pSpan = (Expr*)sqlite3DbMallocZero(db, sizeof(Expr)); TEST_ASSERT_NOT_NULL(pSpan); pSpan->op = TK_SPAN; pSpan->pLeft = pLeft; return pSpan; } /* Initialize a Parse object with the given pNewTable and coldef Token. */ static void initParse(Parse *pParse, sqlite3 *db, Table *pNew, const char *zColDef, Token *pTokOut){ memset(pParse, 0, sizeof(*pParse)); pParse->db = db; db->pParse = pParse; /* Required by sqlite3AlterFinishAddColumn asserts */ pParse->pNewTable = pNew; /* Token for column definition text */ pTokOut->z = (const unsigned char*)zColDef; pTokOut->n = (int)strlen(zColDef); pTokOut->dyn = 0; } /* Cleanup minimal allocations done for pNewTable */ static void freeNewAlterTable(sqlite3 *db, Table *pNew){ if( pNew ){ if( pNew->aCol ){ /* free any default expressions we might have attached */ if( pNew->aCol[0].pDflt ){ sqlite3ExprDelete(db, pNew->aCol[0].pDflt); pNew->aCol[0].pDflt = 0; } sqlite3DbFree(db, pNew->aCol); } if( pNew->zName ) sqlite3DbFree(db, pNew->zName); sqlite3DbFree(db, pNew); } } void setUp(void) { /* fresh in-memory db with base table */ init_db_and_base_table("CREATE TABLE t1(a)"); } void tearDown(void) { if( gDb ){ sqlite3_close(gDb); gDb = NULL; } } /* Test: PRIMARY KEY on new column is rejected immediately with error message */ void test_sqlite3AlterFinishAddColumn_rejects_primary_key(void){ Table *pNew = allocNewAlterTable(gDb, "t1"); Column *pCol = &pNew->aCol[0]; pCol->colFlags |= COLFLAG_PRIMKEY; Parse sParse; Token sTok; initParse(&sParse, gDb, pNew, "x INTEGER PRIMARY KEY", &sTok); sqlite3AlterFinishAddColumn(&sParse, &sTok); TEST_ASSERT_GREATER_THAN(0, sParse.nErr); TEST_ASSERT_NOT_NULL(sParse.zErrMsg); TEST_ASSERT_EQUAL_INT(0, strstr(sParse.zErrMsg, "Cannot add a PRIMARY KEY column")==NULL); freeNewAlterTable(gDb, pNew); } /* Test: UNIQUE constraint on new column is rejected immediately with error message */ void test_sqlite3AlterFinishAddColumn_rejects_unique(void){ Table *pNew = allocNewAlterTable(gDb, "t1"); Column *pCol = &pNew->aCol[0]; pCol->colFlags &= ~COLFLAG_PRIMKEY; /* ensure not primary key */ /* Any non-NULL pIndex indicates UNIQUE column in this code-path */ pNew->pIndex = (Index*)pNew; /* dummy non-NULL */ Parse sParse; Token sTok; initParse(&sParse, gDb, pNew, "x INTEGER UNIQUE", &sTok); sqlite3AlterFinishAddColumn(&sParse, &sTok); TEST_ASSERT_GREATER_THAN(0, sParse.nErr); TEST_ASSERT_NOT_NULL(sParse.zErrMsg); TEST_ASSERT_EQUAL_INT(0, strstr(sParse.zErrMsg, "Cannot add a UNIQUE column")==NULL); /* reset to avoid dangling pIndex into freed memory */ pNew->pIndex = 0; freeNewAlterTable(gDb, pNew); } /* Test: NOT NULL with literal NULL default schedules runtime error (no immediate error, VDBE generated) */ void test_sqlite3AlterFinishAddColumn_notnull_with_null_default_generates_runtime_check(void){ Table *pNew = allocNewAlterTable(gDb, "t1"); Column *pCol = &pNew->aCol[0]; pCol->notNull = 1; /* any non-zero triggers the check */ /* Default is literal NULL: represented as TK_SPAN with pLeft->op == TK_NULL */ pCol->pDflt = makeSpanExprWithLeft(gDb, TK_NULL); Parse sParse; Token sTok; initParse(&sParse, gDb, pNew, "x INTEGER NOT NULL DEFAULT NULL", &sTok); sqlite3AlterFinishAddColumn(&sParse, &sTok); TEST_ASSERT_EQUAL_INT(0, sParse.nErr); TEST_ASSERT_NOT_NULL(sParse.pVdbe); /* VDBE should be created due to nested parse and follow-up code */ freeNewAlterTable(gDb, pNew); } /* Test: STORED generated column is rejected via runtime check path (no immediate error, VDBE generated) */ void test_sqlite3AlterFinishAddColumn_stored_generated_column_rejected_runtime(void){ Table *pNew = allocNewAlterTable(gDb, "t1"); Column *pCol = &pNew->aCol[0]; pCol->colFlags |= (COLFLAG_GENERATED | COLFLAG_STORED); Parse sParse; Token sTok; initParse(&sParse, gDb, pNew, "x GENERATED ALWAYS AS (a+1) STORED", &sTok); sqlite3AlterFinishAddColumn(&sParse, &sTok); TEST_ASSERT_EQUAL_INT(0, sParse.nErr); TEST_ASSERT_NOT_NULL(sParse.pVdbe); freeNewAlterTable(gDb, pNew); } /* Test: Constant default value allowed (no immediate error, VDBE generated) */ void test_sqlite3AlterFinishAddColumn_constant_default_allowed(void){ Table *pNew = allocNewAlterTable(gDb, "t1"); Column *pCol = &pNew->aCol[0]; /* Not generated column */ pCol->colFlags &= ~COLFLAG_GENERATED; /* A simple constant default: represent as TK_SPAN with left TK_INTEGER */ pCol->pDflt = makeSpanExprWithLeft(gDb, TK_INTEGER); Parse sParse; Token sTok; initParse(&sParse, gDb, pNew, "x INTEGER DEFAULT 5", &sTok); sqlite3AlterFinishAddColumn(&sParse, &sTok); TEST_ASSERT_EQUAL_INT(0, sParse.nErr); TEST_ASSERT_NOT_NULL(sParse.pVdbe); freeNewAlterTable(gDb, pNew); } /* Test: Non-constant default leads to runtime check (no immediate error, VDBE generated) */ void test_sqlite3AlterFinishAddColumn_nonconstant_default_generates_runtime_check(void){ Table *pNew = allocNewAlterTable(gDb, "t1"); Column *pCol = &pNew->aCol[0]; /* Not generated */ pCol->colFlags &= ~COLFLAG_GENERATED; /* Non-constant default: represent as TK_SPAN with left TK_FUNCTION (e.g., CURRENT_TIME) */ pCol->pDflt = makeSpanExprWithLeft(gDb, TK_FUNCTION); Parse sParse; Token sTok; initParse(&sParse, gDb, pNew, "x TEXT DEFAULT (CURRENT_TIME)", &sTok); sqlite3AlterFinishAddColumn(&sParse, &sTok); TEST_ASSERT_EQUAL_INT(0, sParse.nErr); TEST_ASSERT_NOT_NULL(sParse.pVdbe); freeNewAlterTable(gDb, pNew); } int main(void){ UNITY_BEGIN(); RUN_TEST(test_sqlite3AlterFinishAddColumn_rejects_primary_key); RUN_TEST(test_sqlite3AlterFinishAddColumn_rejects_unique); RUN_TEST(test_sqlite3AlterFinishAddColumn_notnull_with_null_default_generates_runtime_check); RUN_TEST(test_sqlite3AlterFinishAddColumn_stored_generated_column_rejected_runtime); RUN_TEST(test_sqlite3AlterFinishAddColumn_constant_default_allowed); RUN_TEST(test_sqlite3AlterFinishAddColumn_nonconstant_default_generates_runtime_check); return UNITY_END(); }