diff --git a/manifest b/manifest index 74dd575767..69cc97ee71 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Ensure\sthat\sall\s(parent_key\s=\schild_key)\scomparisons\sperformed\sas\spart\sof\sforeign\skey\sprocessing\sare\sdone\susing\sthe\saffinity\sof\sthe\sparent\skey.\sReport\s[bugs:/info/2026-10-20T02:56:10Z\s|\s2026-10-20T02:56:10Z]. -D 2026-10-21T11:52:56.928 +C An\sattempt\sto\sget\sthe\snew\sforeign\skey\scode\sto\suse\sindexes\son\stype\smismatch\nbetween\sparent\sand\schild. +D 2026-10-21T12:24:59.237 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -760,7 +760,7 @@ F src/dbstat.c 34d4e825fbf763640ce3dbc56910ab080495ef4a6efb48b2b1852a43d43763fc F src/delete.c f3f07c1fb6ac87fac2425d48fd40ecb2f24a4495a9966f5a939668d3576916c8 F src/expr.c 7ff00869afa1d3f11ea5f232c78e7b73a1a33bc1cb29c0af063b768658495234 F src/fault.c 460f3e55994363812d9d60844b2a6de88826e007 -F src/fkey.c 62209d43ea5ac6db3aaa0ab96bec73e19b51e19070e300f7e5c4331359745796 +F src/fkey.c 9b8c8c3033b69fc78b6e94b339ff3b0190fa37f5615d8dfeaaf1fea53ea2471a F src/func.c 8c500a079a52d789613b173ba1894b143f8e38009bab224179bc89d3493201a0 F src/global.c 7eea537ac0c113ac55cb2dd4d8fc2a6a91ae478be34231b4efaffc46817fee85 F src/hash.c 03c8c0f4be9e8bcb6de65aa26d34a61d48a9430747084a69f9469fbb00ea52ca @@ -1206,7 +1206,7 @@ F test/fkey4.test 86446017011273aad8f9a99c1a65019e7bd9ca9d F test/fkey5.test 6727452e163a427147e84e739da18713da553d79f9783559b04fdcd36d5c7421 F test/fkey6.test 668a7299e75899b0a3342c36df655be57f76a05aca3544bda939a6e676e2f000 F test/fkey7.test 64fb28da03da5dfe3cdef5967aa7e832c2507bf7fb8f0780cacbca1f2338d031 -F test/fkey8.test e03321af9e9320de63088cd219af34b4e97a7679e295ee23cdde874301ec276c +F test/fkey8.test e73c52882f0b655ce4e18b131b0117145322c8febfce053a79c3d53638312af8 F test/fkey_malloc.test 594a7ea1fbab553c036c70813cd8bd9407d63749 F test/fordelete.test ba98f14446b310f9c9d935b97ec748753d0144a28b356ba30d1f4f6958fdde5c F test/fork-test.c 9ac2e6423a1d38df3d6be0e8ac15608b545de21e2b19d9d876254c5931b63edb @@ -2295,8 +2295,11 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P decd49e6565bd5105e3b5cbe1067c8992582076a7c4e12cb6f3d089f38aba524 -R a84e136060b7946f27e7ae696367e34b -U dan -Z 340908e2de6a8df53446363a056624d5 +P feb6d3a3016d890fb1a8ca2a1752949b8adc0c06debb6ca305cacafc6b6edd10 +R 6e24683d1e21918b10baf0ff41e12aa6 +T *branch * experimental-fk-opt +T *sym-experimental-fk-opt * +T -sym-trunk * +U drh +Z c7f84733c0d25278056acad5a8fa2792 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.tags b/manifest.tags index bec971799f..70c30ae6d7 100644 --- a/manifest.tags +++ b/manifest.tags @@ -1,2 +1,2 @@ -branch trunk -tag trunk +branch experimental-fk-opt +tag experimental-fk-opt diff --git a/manifest.uuid b/manifest.uuid index 6f25ee9162..87af050de8 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -feb6d3a3016d890fb1a8ca2a1752949b8adc0c06debb6ca305cacafc6b6edd10 +c0523245e2ba3339d8b21cf26e164d05ec252f3d882eadacd4c510a224ccbdba diff --git a/src/fkey.c b/src/fkey.c index cb169c3db0..0a27d7164f 100644 --- a/src/fkey.c +++ b/src/fkey.c @@ -530,7 +530,7 @@ static int fkUsesLhsAffinity(Table *p1, int iCol1, Table *p2, int iCol2){ /* If either side has a numeric affinity, then the comparison uses numeric ** affinity. So the affinity matches if aff1 is numeric, but not if aff1 - ** is not numeric but aff 2 is. */ + ** is not numeric but aff2 is. */ if( sqlite3IsNumericAffinity(aff1) ) return 1; if( sqlite3IsNumericAffinity(aff2) ) return 0; @@ -543,6 +543,86 @@ static int fkUsesLhsAffinity(Table *p1, int iCol1, Table *p2, int iCol2){ return (aff1==aff2 || aff1==SQLITE_AFF_BLOB); } +/* +** Return an expression equivalent to (pLeft = pRight), where pLeft is a +** parent key value and pRight is an unresolved reference to column +** iChildCol of child table pChild. The comparison uses the affinity and +** collation sequence of column iCol of parent table pTab. pLeft must +** already carry that affinity and collation sequence. Ownership of +** both pLeft and pRight passes to this routine. +** +** If the simple (pLeft = pRight) does not naturally use the affinity of +** the parent column, the comparison is coded as (pLeft = +pRight). But +** the unary + prevents any index on the child column from being used. +** So in that case an extra term is added: +** +** (pLeft = +pRight) AND +** (pRight COLLATE IN (+pLeft, +CAST(pLeft AS NUMERIC))) +** +** Each child value that matches pLeft under the affinity of the parent +** column is either equal to pLeft itself or numerically equal to +** CAST(pLeft AS NUMERIC). So the IN term matches a superset of the +** child rows that the exact (pLeft = +pRight) term does, and since it +** compares using the affinity of the child column, it may be used with +** an index on the child column. The unary + operators on the RHS of the +** IN ensure that no affinity other than that of the child column is +** applied. +*/ +static Expr *fkParentChildEq( + Parse *pParse, /* Parsing context */ + Expr *pLeft, /* Parent key value */ + Expr *pRight, /* Unresolved reference to child key column */ + Table *pTab, /* Parent table */ + int iCol, /* Parent key column, or <0 for rowid */ + Table *pChild, /* Child table */ + int iChildCol /* Child key column */ +){ + sqlite3 *db = pParse->db; + Expr *pEq; +#ifndef SQLITE_OMIT_CAST + Expr *pIn = 0; +#endif + + if( fkUsesLhsAffinity(pTab, iCol, pChild, iChildCol) ){ + return sqlite3PExpr(pParse, TK_EQ, pLeft, pRight); + } + assert( iCol>=0 ); + +#ifndef SQLITE_OMIT_CAST + { + static const Token tNumeric = { "NUMERIC", 7 }; + const char *zColl = sqlite3ColumnColl(&pTab->aCol[iCol]); + Expr *pCast; + ExprList *pList; + + pCast = sqlite3ExprAlloc(db, TK_CAST, &tNumeric, 0); + sqlite3ExprAttachSubtrees(db, pCast, sqlite3ExprDup(db, pLeft, 0), 0); + pList = sqlite3ExprListAppend(pParse, 0, + sqlite3PExpr(pParse, TK_UPLUS, sqlite3ExprDup(db, pLeft, 0), 0) + ); + pList = sqlite3ExprListAppend(pParse, pList, + sqlite3PExpr(pParse, TK_UPLUS, pCast, 0) + ); + pIn = sqlite3PExpr(pParse, TK_IN, sqlite3ExprAddCollateString(pParse, + sqlite3ExprDup(db, pRight, 0), zColl ? zColl : sqlite3StrBINARY + ), 0); + if( pIn ){ + pIn->x.pList = pList; + sqlite3ExprSetHeightAndFlags(pParse, pIn); + }else{ + sqlite3ExprListDelete(db, pList); + } + } +#endif + + pRight = sqlite3PExpr(pParse, TK_UPLUS, pRight, 0); + pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight); +#ifndef SQLITE_OMIT_CAST + pEq = sqlite3ExprAnd(pParse, pEq, pIn); +#endif + return pEq; +} + /* ** This function is called to generate code executed when a row is deleted ** from the parent table of foreign key constraint pFKey and, if pFKey is @@ -612,7 +692,7 @@ static void fkScanChildren( Expr *pRight; /* Column ref to child table */ Expr *pEq; /* Expression (pLeft = pRight) */ i16 iCol; /* Index of column in parent table */ - i16 iCol2; /* Index of column in child table */ + i16 iCol2; /* Index of column in child table */ const char *zCol; /* Name of column in child table */ iCol = pIdx ? pIdx->aiColumn[i] : -1; @@ -621,12 +701,8 @@ static void fkScanChildren( assert( iCol2>=0 ); zCol = pFKey->pFrom->aCol[iCol2].zCnName; pRight = sqlite3Expr(db, TK_ID, zCol); - if( 0==fkUsesLhsAffinity(pTab, iCol, pFKey->pFrom, iCol2) ){ - /* In cases where the expression does not naturally use the affinity - ** of the parent column, add a unary + to the child column. */ - pRight = sqlite3PExpr(pParse, TK_UPLUS, pRight, 0); - } - pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight); + pEq = fkParentChildEq(pParse, pLeft, pRight, pTab, iCol, + pFKey->pFrom, iCol2); pWhere = sqlite3ExprAnd(pParse, pWhere, pEq); } @@ -1286,9 +1362,9 @@ static Trigger *fkActionTrigger( Token tToCol; /* Name of column in parent table */ int iFromCol; /* Idx of column in child table */ int iToCol; /* Idx of column in parent table */ - Expr *pEq; /* OLD.tToCol = zFromCol */ + Expr *pEq; /* OLD.tToCol = tFromCol */ Expr *pLeft; /* OLD.tToCol */ - Expr *pRight; /* zFromCol */ + Expr *pRight; /* tFromCol */ iFromCol = aiCol ? aiCol[i] : pFKey->aCol[0].iFrom; assert( iFromCol>=0 ); @@ -1304,20 +1380,16 @@ static Trigger *fkActionTrigger( ); pRight = sqlite3ExprAlloc(db, TK_ID, &tFromCol, 0); - /* OLD.* columns do not normally have an affinity. So set the affinity - ** of OLD.zToCol (the parent key) explicitly here. Also, in cases where - ** (parent_key = child_key) does not naturally use the affinity of the - ** parent column, add a unary + to the child column. */ + /* OLD.* columns do not normally have an affinity. So set the affinity + ** of OLD.tToCol (the parent key) explicitly here. */ if( pLeft ) pLeft->affExpr = pTab->aCol[iToCol].affinity; - if( 0==fkUsesLhsAffinity(pTab, iToCol, pFKey->pFrom, iFromCol) ){ - pRight = sqlite3PExpr(pParse, TK_UPLUS, pRight, 0); - } - /* Create the expression "OLD.zToCol = zFromCol". It is important - ** that the "OLD.zToCol" term is on the LHS of the = operator, so + /* Create the expression "OLD.tToCol = tFromCol". It is important + ** that the "OLD.tToCol" term is on the LHS of the = operator, so ** that the collation sequence associated with the parent table is ** used for the comparison. */ - pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight); + pEq = fkParentChildEq(pParse, pLeft, pRight, pTab, iToCol, + pFKey->pFrom, iFromCol); pWhere = sqlite3ExprAnd(pParse, pWhere, pEq); /* For ON UPDATE, construct the next term of the WHEN clause. diff --git a/test/fkey8.test b/test/fkey8.test index 5ff1eb62a2..536169dc9a 100644 --- a/test/fkey8.test +++ b/test/fkey8.test @@ -358,5 +358,152 @@ do_execsql_test 8.8 { do_execsql_test 8.9 { PRAGMA foreign_key_check } +#------------------------------------------------------------------------- +# More cases where (parent_key = child_key) does not naturally use the +# affinity of the parent key. Each is tested both with and without an +# index on the child key. +# +foreach {tn idx} {1 0 2 1} { + reset_db + execsql { PRAGMA foreign_keys=ON } + + # TEXT parent, INTEGER child. Child value 1001 matches '1001' but + # not '01001'. + do_execsql_test 9.$tn.1 { + CREATE TABLE p1(code TEXT PRIMARY KEY); + CREATE TABLE c1(k INTEGER REFERENCES p1 ON DELETE CASCADE); + INSERT INTO p1 VALUES('01001'), ('1001'); + INSERT INTO c1 VALUES(1001); + } + if {$idx} { execsql { CREATE INDEX c1k ON c1(k) } } + do_execsql_test 9.$tn.2 { + DELETE FROM p1 WHERE code='01001'; + SELECT quote(k) FROM c1; + } {1001} + do_execsql_test 9.$tn.3 { + DELETE FROM p1 WHERE code='1001'; + SELECT quote(k) FROM c1; + } {} + + # TEXT parent, REAL child. Child value 1001.0 matches '1001.0' but + # not '1001'. + do_execsql_test 9.$tn.4 { + CREATE TABLE p2(code TEXT PRIMARY KEY); + CREATE TABLE c2(k REAL REFERENCES p2 ON DELETE SET NULL); + INSERT INTO p2 VALUES('1001'), ('1001.0'); + INSERT INTO c2 VALUES(1001); + } + if {$idx} { execsql { CREATE INDEX c2k ON c2(k) } } + do_execsql_test 9.$tn.5 { + DELETE FROM p2 WHERE code='1001'; + SELECT quote(k) FROM c2; + } {1001.0} + do_execsql_test 9.$tn.6 { + DELETE FROM p2 WHERE code='1001.0'; + SELECT quote(k) FROM c2; + } {NULL} + + # BLOB parent, INTEGER child. Child value 5 matches 5 but not '5'. + do_execsql_test 9.$tn.7 { + CREATE TABLE p3(code UNIQUE); + CREATE TABLE c3(k INTEGER REFERENCES p3(code) ON DELETE CASCADE); + INSERT INTO p3 VALUES('5'), (5); + INSERT INTO c3 VALUES(5); + } + if {$idx} { execsql { CREATE INDEX c3k ON c3(k) } } + do_execsql_test 9.$tn.8 { + DELETE FROM p3 WHERE code='5'; + SELECT quote(k) FROM c3; + } {5} + do_execsql_test 9.$tn.9 { + DELETE FROM p3 WHERE code=5; + SELECT quote(k) FROM c3; + } {} + + # TEXT parent, BLOB child, ON UPDATE CASCADE. + do_execsql_test 9.$tn.10 { + CREATE TABLE p4(code TEXT PRIMARY KEY); + CREATE TABLE c4(k REFERENCES p4 ON UPDATE CASCADE); + INSERT INTO p4 VALUES('7'), ('07'); + INSERT INTO c4 VALUES(7), ('07'); + } + if {$idx} { execsql { CREATE INDEX c4k ON c4(k) } } + do_execsql_test 9.$tn.11 { + UPDATE p4 SET code='8' WHERE code='7'; + SELECT quote(k) FROM c4 ORDER BY 1; + } {'07' '8'} + + # TEXT parent with NOCASE collation, BLOB child. + do_execsql_test 9.$tn.12 { + CREATE TABLE p5(code TEXT COLLATE nocase PRIMARY KEY); + CREATE TABLE c5(k REFERENCES p5 ON DELETE CASCADE); + INSERT INTO p5 VALUES('abc'), ('10'); + INSERT INTO c5 VALUES('ABC'), (10); + } + if {$idx} { execsql { CREATE INDEX c5k ON c5(k COLLATE nocase) } } + do_execsql_test 9.$tn.13 { + DELETE FROM p5; + SELECT count(*) FROM c5; + } {0} + + # Immediate constraint (no action) on a TEXT parent with a BLOB child. + do_execsql_test 9.$tn.14 { + CREATE TABLE p6(code TEXT PRIMARY KEY); + CREATE TABLE c6(k REFERENCES p6); + INSERT INTO p6 VALUES('1'), ('01'); + INSERT INTO c6 VALUES(1); + } + if {$idx} { execsql { CREATE INDEX c6k ON c6(k) } } + do_execsql_test 9.$tn.15 { + DELETE FROM p6 WHERE code='01'; + } {} + do_catchsql_test 9.$tn.16 { + DELETE FROM p6 WHERE code='1'; + } {1 {FOREIGN KEY constraint failed}} + + do_execsql_test 9.$tn.17 { PRAGMA foreign_key_check } +} + +#------------------------------------------------------------------------- +# Check that an index on the child key can still be used when the +# comparison does not naturally use the affinity of the parent key. +# +reset_db +do_execsql_test 10.0 { + PRAGMA foreign_keys=ON; + CREATE TABLE parent(id INTEGER PRIMARY KEY, code TEXT UNIQUE); + CREATE TABLE c1(id INTEGER PRIMARY KEY, k REFERENCES parent(code)); + CREATE INDEX c1k ON c1(k); + CREATE TABLE c2(id INTEGER PRIMARY KEY, + k REFERENCES parent(code) ON DELETE CASCADE + ); + CREATE INDEX c2k ON c2(k); + CREATE TABLE c3(k INTEGER PRIMARY KEY REFERENCES parent(code) + ON UPDATE CASCADE + ); +} + +do_eqp_test 10.1 { + DELETE FROM parent WHERE id=1; +} { + QUERY PLAN + |--SEARCH parent USING INTEGER PRIMARY KEY (rowid=?) + |--SEARCH c3 USING INTEGER PRIMARY KEY (rowid=?) + |--SEARCH c2 USING COVERING INDEX c2k (k=?) + `--SEARCH c1 USING COVERING INDEX c1k (k=?) +} + +do_eqp_test 10.2 { + UPDATE parent SET code='x' WHERE id=1; +} { + QUERY PLAN + |--SEARCH parent USING INTEGER PRIMARY KEY (rowid=?) + |--SEARCH c3 USING INTEGER PRIMARY KEY (rowid=?) + |--SEARCH c2 USING COVERING INDEX c2k (k=?) + |--SEARCH c1 USING COVERING INDEX c1k (k=?) + |--SEARCH c3 USING INTEGER PRIMARY KEY (rowid=?) + |--SEARCH c2 USING COVERING INDEX c2k (k=?) + `--SEARCH c1 USING COVERING INDEX c1k (k=?) +} finish_test