From e4facb4585c7ef4d1e314e1314b81512408aeab1 Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 15 Oct 2026 15:33:09 +0000 Subject: [PATCH] When a single loop uses an IN(...) expression and a LIKE/GLOB expression with an index, nest the 2-iteration loop that scans ranges of text and blob values using the LIKE/GLOB pattern inside the IN(...) loop in order to ensure that results are scanned in index order. Report [bugs:/info/2026-10-13T18:54:28Z | 2026-10-13T18:54:28Z]. FossilOrigin-Name: 08e979525ff1eef80c8e824c80cca7b6a3dfb320122258803673ace43c806c9a --- manifest | 17 ++-- manifest.uuid | 2 +- src/where.c | 28 +++++-- src/wherecode.c | 17 +++- test/like4.test | 216 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 20 deletions(-) create mode 100644 test/like4.test diff --git a/manifest b/manifest index 25ae924d45..ac9aa4859d 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Remove\scode\sthat\sis\sno\slonger\sreachable\safter\s[bbdd13370de93035].\s\sAdd\nan\sassert()\sto\sverify\sthat\sthe\scode\sis\sunreachable. -D 2026-10-15T13:55:05.210 +C When\sa\ssingle\sloop\suses\san\sIN(...)\sexpression\sand\sa\sLIKE/GLOB\sexpression\swith\san\sindex,\snest\sthe\s2-iteration\sloop\sthat\sscans\sranges\sof\stext\sand\sblob\svalues\susing\sthe\sLIKE/GLOB\spattern\sinside\sthe\sIN(...)\sloop\sin\sorder\sto\sensure\sthat\sresults\sare\sscanned\sin\sindex\sorder.\sReport\s[bugs:/info/2026-10-13T18:54:28Z\s|\s2026-10-13T18:54:28Z]. +D 2026-10-15T15:33:09.085 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -886,9 +886,9 @@ F src/vxworks.h 9d18819c5235b49c2340a8a4d48195ec5d5afb637b152406de95a9436beeaeab F src/wal.c 41438914e455bbc42ccff8545eab4c711f91233e480337e44a3d5fdf121fce74 F src/wal.h c6251cfc1950643a976625ca452e46dc9b466b4f5dd2e85f244f29f79bf68ace F src/walker.c d5006d6b005e4ea7302ad390957a8d41ed83faa177e412f89bc5600a7462a014 -F src/where.c 21926754d085b9689b71bf6188810c0c59f7ad5ec05c60aedf461e765217c350 +F src/where.c e3373734cf74e7b52a786db5d14beb8d73b937bd947d7eff1a9dc360c52745e7 F src/whereInt.h 5d739acb94bf1fdf6edd502a919e3dcf2d340dda63cc1827304e119ea878f6f3 -F src/wherecode.c 6d4835272cf65ba13379a1620a806d1ef1c2e0f7d6a661ed6ee40f013f357189 +F src/wherecode.c 69ff3d0fe8697d2ca482d25c15dfdc08cf8e4605ab5ab297e9d12b3dbc6812fd F src/whereexpr.c 76f72a95c5cf0e8a8edb679ca4fb7afa17ba45ff4effe73c414266510109e701 F src/window.c 79a17de595e62aa45961ef1cbe3392e7ebae69fdde3fd0b0200254bc4647025e F test/8_3_names.test ebbb5cd36741350040fd28b432ceadf495be25b2 @@ -1456,6 +1456,7 @@ F test/lemon-test01.y 70110eff607ab137ccc851edb2bc7e14a6d4f246b5d2d25f82a60b69d8 F test/like.test 0036f8fe548fceabd1496361bfa262f35cf5ed17c794bd15e22e9f3de12e0eb0 F test/like2.test d3be15fefee3e02fc88942a9b98f26c5339bbdef7783c90023c092c4955fe3d3 F test/like3.test 3c9be7a0122908d8ead6aa25e47c2b9787eea88e8062926078ea7b3e95c71d99 +F test/like4.test 3312c44c9e09d94f8e10a2158a63b615eb6ee09afc4aecbd385c31271824f92a F test/limit.test 350f5d03c29e7dff9a2cde016f84f8d368d40bcd02fa2b2a52fa10c4bf3cbfaf F test/limit2.test 621188fc3e5c3b8d2ef9827e05fa8313792ae563579073136efd25cb65325f1b F test/literal.test a65dca9fef86e51b8e45544268e37abbd4bb94ba35fd65f6fdcab2f288cd8f79 @@ -2289,8 +2290,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P 3ce64081cab882b60ad73db77da961e479cd84eb151444f420da9fcc370f740d -R 76f43338ac789d41d3609cb388d0106d -U drh -Z 03d6a8e5c145c41269fb248d2afbccb9 +P a0f9daf75b2fc0b230e9d2b79eab9e495a911f39d7427eb5ec09f5414d8703b7 +R 3fedb2628e1212451a990a8e79205119 +U dan +Z 6307e8e29ee9911f6e6ceee7d948f13d # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index d62b061697..32b5ef8557 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -a0f9daf75b2fc0b230e9d2b79eab9e495a911f39d7427eb5ec09f5414d8703b7 +08e979525ff1eef80c8e824c80cca7b6a3dfb320122258803673ace43c806c9a diff --git a/src/where.c b/src/where.c index 38c44fcb5c..5099b846c2 100644 --- a/src/where.c +++ b/src/where.c @@ -7664,10 +7664,29 @@ void sqlite3WhereEnd(WhereInfo *pWInfo){ VdbeCoverage(v); } } + if( pLevel->addrNxt!=pLevel->addrBrk ){ + /* This occurs in two cases - (a) for a LIKE/GLOB scan that must visit + ** ranges of text and blob values separately, and (b) when there is + ** an IN(...) constraint driving the index. This is asserted() in the + ** next block, inside the #ifndef. */ + sqlite3VdbeResolveLabel(v, pLevel->addrNxt); + } +#ifndef SQLITE_LIKE_DOESNT_MATCH_BLOBS + assert( (pLevel->addrNxt==pLevel->addrBrk) + || (pLevel->addrLikeRep) /* (a) */ + || ((pLoop->wsFlags & WHERE_IN_ABLE) && pLevel->u.in.nIn>0) /* (b) */ + ); + if( pLevel->addrLikeRep ){ + /* The LIKE optimization loop is nested inside any IN(...) and + ** skip-scan loops, so it is closed here, before either of those. */ + sqlite3VdbeAddOp2(v, OP_DecrJumpZero, (int)(pLevel->iLikeRepCntr>>1), + pLevel->addrLikeRep); + VdbeCoverage(v); + } +#endif if( (pLoop->wsFlags & WHERE_IN_ABLE)!=0 && pLevel->u.in.nIn>0 ){ struct InLoop *pIn; int j; - sqlite3VdbeResolveLabel(v, pLevel->addrNxt); for(j=pLevel->u.in.nIn, pIn=&pLevel->u.in.aInLoop[j-1]; j>0; j--, pIn--){ assert( sqlite3VdbeGetOp(v, pIn->addrInTop+1)->opcode==OP_IsNull || pParse->db->mallocFailed ); @@ -7719,13 +7738,6 @@ void sqlite3WhereEnd(WhereInfo *pWInfo){ sqlite3VdbeJumpHere(v, pLevel->addrSkip); sqlite3VdbeJumpHere(v, pLevel->addrSkip-2); } -#ifndef SQLITE_LIKE_DOESNT_MATCH_BLOBS - if( pLevel->addrLikeRep ){ - sqlite3VdbeAddOp2(v, OP_DecrJumpZero, (int)(pLevel->iLikeRepCntr>>1), - pLevel->addrLikeRep); - VdbeCoverage(v); - } -#endif if( pLevel->iLeftJoin ){ int ws = pLoop->wsFlags; addr = sqlite3VdbeAddOp1(v, OP_IfPos, pLevel->iLeftJoin); VdbeCoverage(v); diff --git a/src/wherecode.c b/src/wherecode.c index 99d5b877a0..0ee1d90bb9 100644 --- a/src/wherecode.c +++ b/src/wherecode.c @@ -2093,9 +2093,6 @@ Bitmask sqlite3WhereCodeOneLoopStart( assert( pRangeStart!=0 ); /* LIKE opt constraints */ assert( pRangeStart->wtFlags & TERM_LIKEOPT ); /* occur in pairs */ pLevel->iLikeRepCntr = (u32)++pParse->nMem; - sqlite3VdbeAddOp2(v, OP_Integer, 1, (int)pLevel->iLikeRepCntr); - VdbeComment((v, "LIKE loop counter")); - pLevel->addrLikeRep = sqlite3VdbeCurrentAddr(v); /* iLikeRepCntr actually stores 2x the counter register number. The ** bottom bit indicates whether the search order is ASC or DESC. */ testcase( bRev ); @@ -2159,6 +2156,20 @@ Bitmask sqlite3WhereCodeOneLoopStart( codeCursorHint(pTabItem, pWInfo, pLevel, pRangeEnd); regBase = codeAllEqualityTerms(pParse,pLevel,bRev,nExtraReg,&zStartAff); assert( zStartAff==0 || sqlite3Strlen30(zStartAff)>=nEq ); +#ifndef SQLITE_LIKE_DOESNT_MATCH_BLOBS + if( pLevel->iLikeRepCntr ){ + /* The LIKE optimization loop must be the innermost of any IN(...) + ** or skip-scan loops. Otherwise rows may not be visited in index + ** order. This means it needs an addrNxt label for the same reason + ** IN(...) scans do. */ + if( pLevel->addrNxt==pLevel->addrBrk ){ + pLevel->addrNxt = sqlite3VdbeMakeLabel(pParse); + } + sqlite3VdbeAddOp2(v, OP_Integer, 1, (int)(pLevel->iLikeRepCntr>>1)); + VdbeComment((v, "LIKE loop counter")); + pLevel->addrLikeRep = sqlite3VdbeCurrentAddr(v); + } +#endif if( zStartAff && nTop ){ zEndAff = sqlite3DbStrDup(db, &zStartAff[nEq]); } diff --git a/test/like4.test b/test/like4.test new file mode 100644 index 0000000000..c936cabf63 --- /dev/null +++ b/test/like4.test @@ -0,0 +1,216 @@ +# 2026-10-15 +# +# 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. +# +#*********************************************************************** +# +# This file implements regression tests for SQLite library. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix like4 + +ifcapable !like_match_blobs { + finish_test + return +} + +do_execsql_test 1.0 { + CREATE TABLE t0(a, c); + INSERT INTO t0 VALUES(1, 'A'); + INSERT INTO t0 VALUES(2, 'A'); + INSERT INTO t0 VALUES(1, X'41'); + INSERT INTO t0 VALUES(2, X'41'); +} + +foreach {tn idx} { + 1 "" + 2 "CREATE INDEX idx ON t0(a, c)" + 3 "CREATE INDEX idx ON t0(c, a)" +} { + + execsql { DROP INDEX IF EXISTS idx } + execsql $idx + + do_execsql_test 1.$tn.1 { + SELECT a, count(*) FROM t0 WHERE a IN (1,2) AND c GLOB 'A' GROUP BY a; + } {1 2 2 2} + + do_execsql_test 1.$tn.2 { + SELECT a, quote(c) FROM t0 WHERE a IN (1,2) AND c GLOB 'A' ORDER BY a; + } { + 1 'A' + 1 X'41' + 2 'A' + 2 X'41' + } +} + +#------------------------------------------------------------------------- +reset_db + +do_execsql_test 2.0 { + CREATE TABLE t1(a, b); + CREATE INDEX t1b ON t1(b); + + INSERT INTO t1 VALUES(1, 'abc'); + INSERT INTO t1 VALUES(2, X'616263'); + INSERT INTO t1 VALUES(3, 'abd'); + INSERT INTO t1 VALUES(4, X'616264'); +} + +do_execsql_test 2.1 { + SELECT a FROM t1 WHERE b GLOB 'ab*' ORDER BY b +} {1 3 2 4} +do_execsql_test 2.2 { + SELECT a FROM t1 WHERE b GLOB 'ab*' ORDER BY b DESC +} {4 2 3 1} + +#------------------------------------------------------------------------- +reset_db + +do_execsql_test 3.0 { + CREATE TABLE t(a, c); + CREATE INDEX ti ON t(a, c); + WITH RECURSIVE n(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM n WHERE i<100) + INSERT INTO t SELECT i%3, 'x'||i FROM n; + INSERT INTO t VALUES(2, 'A'), (1, x'41'); +} + +do_execsql_test 3.1 { + SELECT a, quote(c) FROM t WHERE c GLOB 'A' ORDER BY a; +} { + 1 X'41' + 2 'A' +} + +do_execsql_test 3.2 { + ANALYZE; +} + +do_execsql_test 3.3 { + SELECT a, quote(c) FROM t WHERE c GLOB 'A' ORDER BY a; +} { + 1 X'41' + 2 'A' +} + +#------------------------------------------------------------------------- +# Skip-scan on combined with IN(...) and the LIKE optimization. +# +reset_db + +do_execsql_test 4.0 { + CREATE TABLE t(a, b, c); + CREATE INDEX ti ON t(a, b, c); + WITH RECURSIVE n(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM n WHERE i<300) + INSERT INTO t SELECT i%3, i%2, 'x'||i FROM n; + INSERT INTO t VALUES(2, 0, 'A'), (2, 1, x'41'); + INSERT INTO t VALUES(1, 1, 'A'), (1, 0, x'41'); + INSERT INTO t VALUES(0, 1, 'A'); + ANALYZE; +} + +do_eqp_test 4.1 { + SELECT a, b, quote(c) FROM t WHERE b IN (0,1) AND c GLOB 'A' ORDER BY a, b; +} {SEARCH t USING COVERING INDEX ti (ANY(a) AND b=? AND c>? AND c? AND y