mirror of
https://github.com/sqlite/sqlite.git
synced 2026-10-23 21:49:02 +03:00
An attempt to get the new foreign key code to use indexes on type mismatch
between parent and child. FossilOrigin-Name: c0523245e2ba3339d8b21cf26e164d05ec252f3d882eadacd4c510a224ccbdba
This commit is contained in:
19
manifest
19
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.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
branch trunk
|
||||
tag trunk
|
||||
branch experimental-fk-opt
|
||||
tag experimental-fk-opt
|
||||
|
||||
@@ -1 +1 @@
|
||||
feb6d3a3016d890fb1a8ca2a1752949b8adc0c06debb6ca305cacafc6b6edd10
|
||||
c0523245e2ba3339d8b21cf26e164d05ec252f3d882eadacd4c510a224ccbdba
|
||||
|
||||
112
src/fkey.c
112
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 <coll> 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.
|
||||
|
||||
147
test/fkey8.test
147
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
|
||||
|
||||
Reference in New Issue
Block a user