]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Fix further issues with multi-column IN(...) operators. Also some error handling...
authordan <dan@noemail.net>
Thu, 28 Jul 2016 19:47:15 +0000 (19:47 +0000)
committerdan <dan@noemail.net>
Thu, 28 Jul 2016 19:47:15 +0000 (19:47 +0000)
FossilOrigin-Name: cc3f6542bec99b00d2698889bcea2aa0b587efa0

manifest
manifest.uuid
src/expr.c
src/whereexpr.c
test/rowvalue4.test [new file with mode: 0644]

index 98dcf120d990da12357741762c753d68d2994a09..a1321d1be144d0976cdc645ffb09c3cfddf3244f 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Merge\slatest\strunk\schanges\sinto\sthis\sbranch.
-D 2016-07-28T13:59:21.728
+C Fix\sfurther\sissues\swith\smulti-column\sIN(...)\soperators.\sAlso\ssome\serror\shandling\scases\ssurrounding\srow\svalues.
+D 2016-07-28T19:47:15.803
 F Makefile.in 6c20d44f72d4564f11652b26291a214c8367e5db
 F Makefile.linux-gcc 7bc79876b875010e8c8f9502eb935ca92aa3c434
 F Makefile.msc d66d0395c38571aab3804f8db0fa20707ae4609a
@@ -337,7 +337,7 @@ F src/ctime.c 61949e83c4c36e37195a8398ebc752780b534d95
 F src/date.c 1cc9fb516ec9932c6fd4d2a0d2f8bc4480145c39
 F src/dbstat.c 4f6f7f52b49beb9636ffbd517cfe44a402ba4ad0
 F src/delete.c 4aba4214a377ce8ddde2d2e609777bcc8235200f
-F src/expr.c 82bc40bb27bc68b45303b560ab14eb16e39786a7
+F src/expr.c 473ce0ac25868b750f3bf9f59bbe7e8f06a0bdd5
 F src/fault.c 160a0c015b6c2629d3899ed2daf63d75754a32bb
 F src/fkey.c bc4145347595b7770f9a598cff1c848302cf5413
 F src/func.c 61a4114cf7004f10c542cfabbab9f2bcb9033045
@@ -466,7 +466,7 @@ F src/walker.c 0f142b5bd3ed2041fc52d773880748b212e63354
 F src/where.c df58d6ad7878a08aa96c652ccbc6d0949f8fa472
 F src/whereInt.h 14dd243e13b81cbb0a66063d38b70f93a7d6e613
 F src/wherecode.c 3aff7683566af3428f865904aafa7efb1fbd8701
-F src/whereexpr.c b896f8ff6a53cbd3daaee84ec33e39098762bb46
+F src/whereexpr.c bc85d04c6751ca40cab99a10de308e44893c76b9
 F test/8_3_names.test ebbb5cd36741350040fd28b432ceadf495be25b2
 F test/affinity2.test a6d901b436328bd67a79b41bb0ac2663918fe3bd
 F test/aggerror.test a867e273ef9e3d7919f03ef4f0e8c0d2767944f2
@@ -1021,6 +1021,7 @@ F test/rowid.test 5b7509f384f4f6fae1af3c8c104c8ca299fea18d
 F test/rowvalue.test 979738b3d49f1d93e3fee56a71d4446217917abc
 F test/rowvalue2.test 8d5dfe75b8f4d1868a2f91f0356f20d36cba64ff
 F test/rowvalue3.test 587c1056016fa6b7a40a9bf6cb85d5443fa47d96
+F test/rowvalue4.test 4480898d62d6813e3e38d9d38c02b9a0be5f94be
 F test/rtree.test 0c8d9dd458d6824e59683c19ab2ffa9ef946f798
 F test/run-wordcount.sh 891e89c4c2d16e629cd45951d4ed899ad12afc09
 F test/savepoint.test c671fdbd34cd3bfe1518a777526ada595180cf8d
@@ -1511,7 +1512,7 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P 719a3b2035a335ca8b9704646b1d641011e3ea0e 6feff15cae8f0427be790355841d49c479c1c586
-R 3c287799720eaa100d938a8b5fd4645d
+P 9685880f7baeb670739fdcf2d9df08e22abaa699
+R 66ab918b4b1a726fe3dea7b50bb8fdb0
 U dan
-Z 3b75e2ba5b9150b77f0261ae152b7ba4
+Z 5baa8fdab10ded1fdce6b30d35067148
index 88af2bcaa6ad7b237f447fe95e29e6e5079f4ad3..c951167b81372f1ed3818286972bee967e6a6ae0 100644 (file)
@@ -1 +1 @@
-9685880f7baeb670739fdcf2d9df08e22abaa699
\ No newline at end of file
+cc3f6542bec99b00d2698889bcea2aa0b587efa0
\ No newline at end of file
index 26587309f26f8f3129de1b9762adf6db09134dac..2921898d3575320d708e3743c172b98723afa716 100644 (file)
@@ -1730,7 +1730,7 @@ int sqlite3IsRowid(const char *z){
 **
 */
 #ifndef SQLITE_OMIT_SUBQUERY
-static Select *isCandidateForInOpt(Expr *pX, int bNullSensitive){
+static Select *isCandidateForInOpt(Expr *pX){
   Select *p;
   SrcList *pSrc;
   ExprList *pEList;
@@ -1759,16 +1759,11 @@ static Select *isCandidateForInOpt(Expr *pX, int bNullSensitive){
   if( IsVirtual(pTab) ) return 0;        /* FROM clause not a virtual table */
   pEList = p->pEList;
 
-  /* All SELECT results must be columns. If the SELECT returns more than
-  ** one column and the bNullSensitive flag is set, all returned columns
-  ** must be declared NOT NULL. */
+  /* All SELECT results must be columns. */
   for(i=0; i<pEList->nExpr; i++){
     Expr *pRes = pEList->a[i].pExpr;
     if( pRes->op!=TK_COLUMN ) return 0;
     assert( pRes->iTable==pSrc->a[0].iCursor );  /* Not a correlated subquery */
-    if( pEList->nExpr>1 && bNullSensitive ){
-      if( pTab->aCol[pRes->iColumn].notNull==0 ) return 0;
-    }
   }
   return p;
 }
@@ -1904,11 +1899,27 @@ int sqlite3FindInIndex(
   assert( pX->op==TK_IN );
   mustBeUnique = (inFlags & IN_INDEX_LOOP)!=0;
 
+  /* If the RHS of this IN(...) operator is a SELECT, and if it matters 
+  ** whether or not the SELECT result contains NULL values, check whether
+  ** or not NULL is actuall possible (it may not be, for example, due 
+  ** to NOT NULL constraints in the schema). If no NULL values are possible,
+  ** set prRhsHasNull to 0 before continuing.
+  */
+  if( prRhsHasNull && (pX->flags & EP_xIsSelect) ){
+    int i;
+    ExprList *pEList = pX->x.pSelect->pEList;
+    for(i=0; i<pEList->nExpr; i++){
+      if( sqlite3ExprCanBeNull(pEList->a[i].pExpr) ) break;
+    }
+    if( i==pEList->nExpr ){
+      prRhsHasNull = 0;
+    }
+  }
+
   /* Check to see if an existing table or index can be used to
   ** satisfy the query.  This is preferable to generating a new 
-  ** ephemeral table.
-  */
-  if( pParse->nErr==0 && (p = isCandidateForInOpt(pX, prRhsHasNull!=0))!=0 ){
+  ** ephemeral table.  */
+  if( pParse->nErr==0 && (p = isCandidateForInOpt(pX))!=0 ){
     sqlite3 *db = pParse->db;              /* Database connection */
     Table *pTab;                           /* Table <table>. */
     i16 iDb;                               /* Database idx for pTab */
@@ -1996,16 +2007,16 @@ int sqlite3FindInIndex(
           assert( IN_INDEX_INDEX_DESC == IN_INDEX_INDEX_ASC+1 );
           eType = IN_INDEX_INDEX_ASC + pIdx->aSortOrder[0];
 
-          if( prRhsHasNull && nExpr==1 
-           && !pTab->aCol[pEList->a[0].pExpr->iColumn].notNull 
-          ){
+          if( prRhsHasNull ){
+            *prRhsHasNull = ++pParse->nMem;
 #ifdef SQLITE_ENABLE_COLUMN_USED_MASK
             i64 mask = (1<<nExpr)-1;
             sqlite3VdbeAddOp4Dup8(v, OP_ColumnsUsed, 
                 iTab, 0, 0, (u8*)&mask, P4_INT64);
 #endif
-            *prRhsHasNull = ++pParse->nMem;
-            sqlite3SetHasNullFlag(v, iTab, *prRhsHasNull);
+            if( nExpr==1 ){
+              sqlite3SetHasNullFlag(v, iTab, *prRhsHasNull);
+            }
           }
           sqlite3VdbeJumpHere(v, iAddr);
         }
@@ -2352,6 +2363,32 @@ int sqlite3CodeSubselect(
 }
 #endif /* SQLITE_OMIT_SUBQUERY */
 
+#ifndef SQLITE_OMIT_SUBQUERY
+/*
+** Expr pIn is an IN(...) expression. This function checks that the 
+** sub-select on the RHS of the IN() operator has the same number of 
+** columns as the vector on the LHS. Or, if the RHS of the IN() is not 
+** a sub-query, that the LHS is a vector of size 1.
+*/
+int sqlite3ExprCheckIN(Parse *pParse, Expr *pIn){
+  int nVector = sqlite3ExprVectorSize(pIn->pLeft);
+  if( (pIn->flags & EP_xIsSelect) ){
+    if( nVector!=pIn->x.pSelect->pEList->nExpr ){
+      sqlite3SubselectError(pParse, pIn->x.pSelect->pEList->nExpr, nVector);
+      return 1;
+    }
+  }else if( nVector!=1 ){
+    if( (pIn->pLeft->flags & EP_xIsSelect) ){
+      sqlite3SubselectError(pParse, nVector, 1);
+    }else{
+      sqlite3ErrorMsg(pParse, "invalid use of row value");
+    }
+    return 1;
+  }
+  return 0;
+}
+#endif
+
 #ifndef SQLITE_OMIT_SUBQUERY
 /*
 ** Generate code for an IN expression.
@@ -2387,6 +2424,7 @@ static void sqlite3ExprCodeIN(
   Expr *pLeft = pExpr->pLeft;
   int i;
 
+  if( sqlite3ExprCheckIN(pParse, pExpr) ) return;
   nVector = sqlite3ExprVectorSize(pExpr->pLeft);
   aiMap = (int*)sqlite3DbMallocZero(
       pParse->db, nVector*(sizeof(int) + sizeof(char)) + 1
@@ -2394,6 +2432,7 @@ static void sqlite3ExprCodeIN(
   if( !aiMap ) return;
   zAff = (char*)&aiMap[nVector];
 
+
   /* Attempt to compute the RHS. After this step, if anything other than
   ** IN_INDEX_NOOP is returned, the table opened ith cursor pExpr->iTable 
   ** contains the values that make up the RHS. If IN_INDEX_NOOP is returned,
@@ -2479,8 +2518,15 @@ static void sqlite3ExprCodeIN(
     sqlite3ReleaseTempReg(pParse, regCkNull);
   }else{
   
-    /* If the LHS is NULL, then the result is either false or NULL depending
-    ** on whether the RHS is empty or not, respectively.  */
+    /* If any value on the LHS is NULL, the result of the IN(...) operator
+    ** must be either false or NULL. If these two are handled identically,
+    ** test the LHS for NULLs and jump directly to destIfNull if any are
+    ** found. 
+    **
+    ** Otherwise, if NULL and false are handled differently, and the
+    ** IN(...) operation is not a vector operation, and the LHS of the
+    ** operator is NULL, then the result is false if the index is 
+    ** completely empty, or NULL otherwise.  */
     if( destIfNull==destIfFalse ){
       for(i=0; i<nVector; i++){
         Expr *p = exprVectorField(pExpr->pLeft, i);
@@ -2497,70 +2543,62 @@ static void sqlite3ExprCodeIN(
     }
   
     if( eType==IN_INDEX_ROWID ){
-      /* In this case, the RHS is the ROWID of table b-tree
-      */
+      /* In this case, the RHS is the ROWID of table b-tree */
       sqlite3VdbeAddOp3(v, OP_SeekRowid, pExpr->iTable, destIfFalse, r1);
       VdbeCoverage(v);
-    }else if( nVector>1 && eType==IN_INDEX_EPH && destIfNull!=destIfFalse ){
-      int regNull = sqlite3GetTempReg(pParse);
-      int r2 = sqlite3GetTempReg(pParse);
-      int r3 = sqlite3GetTempReg(pParse);
-      int r4 = sqlite3GetTempReg(pParse);
-      int addrNext;
-      int addrIf;
-
-      if( destIfFalse!=destIfNull ){
-        sqlite3VdbeAddOp2(v, OP_Integer, 0, regNull);
-      }
-      addrNext = sqlite3VdbeAddOp2(v, OP_Rewind, pExpr->iTable, destIfFalse);
-      for(i=0; i<nVector; i++){
-        Expr *p;
-        CollSeq *pColl;
-        p = exprVectorField(pLeft, i);
-        pColl = sqlite3ExprCollSeq(pParse, p);
-
-        sqlite3VdbeAddOp3(v, OP_Column, pExpr->iTable, i, r2);
-        sqlite3VdbeAddOp4(v, OP_Eq, r1+i, i?r3:r4, r2, (void*)pColl,P4_COLLSEQ);
-        sqlite3VdbeChangeP5(v, SQLITE_STOREP2);
-        if( i!=0 ){
-          sqlite3VdbeAddOp3(v, OP_And, r3, r4, r4);
-        }
-      }
-      addrIf = sqlite3VdbeAddOp1(v, OP_If, r4);
-      if( destIfNull!=destIfFalse ){
-        sqlite3VdbeAddOp2(v, OP_IfNot, r4, sqlite3VdbeCurrentAddr(v)+2);
-        sqlite3VdbeAddOp2(v, OP_Integer, 1, regNull);
-      }
-      sqlite3VdbeAddOp2(v, OP_Next, pExpr->iTable, addrNext+1);
-      if( destIfNull!=destIfFalse ){
-        sqlite3VdbeAddOp2(v, OP_If, regNull, destIfNull);
-      }
-      sqlite3VdbeAddOp2(v, OP_Goto, 0, destIfFalse);
-      sqlite3VdbeChangeP2(v, addrIf, sqlite3VdbeCurrentAddr(v));
-      sqlite3ReleaseTempReg(pParse, regNull);
-      sqlite3ReleaseTempReg(pParse, r2);
-      sqlite3ReleaseTempReg(pParse, r3);
-      sqlite3ReleaseTempReg(pParse, r4);
     }else{
-      /* In this case, the RHS is an index b-tree.
-      */
+      /* In this case, the RHS is an index b-tree. Apply the comparison
+      ** affinities to each value on the LHS of the operator.  */
       sqlite3VdbeAddOp4(v, OP_Affinity, r1, nVector, 0, zAff, nVector);
-  
-      /* If the set membership test fails, then the result of the 
-      ** "x IN (...)" expression must be either 0 or NULL. If the set
-      ** contains no NULL values, then the result is 0. If the set 
-      ** contains one or more NULL values, then the result of the
-      ** expression is also NULL.
-      */
-      assert( destIfFalse!=destIfNull || rRhsHasNull==0 );
-      if( rRhsHasNull==0 ){
+      
+      if( nVector>1 && destIfNull!=destIfFalse ){
+        int iIdx = pExpr->iTable;
+        int addr;
+        int addrNext;
+
+        /* Search the index for the key. */
+        addr = sqlite3VdbeAddOp4Int(v, OP_Found, iIdx, 0, r1, nVector);
+
+        /* At this point the specified key is not present in the index, 
+        ** so the result of the IN(..) operator must be either NULL or
+        ** 0. The vdbe code generated below figures out which.  */
+        addrNext = 1+sqlite3VdbeAddOp2(v, OP_Rewind, iIdx, destIfFalse);
+
+        for(i=0; i<nVector; i++){
+          Expr *p;
+          CollSeq *pColl;
+          int r2 = sqlite3GetTempReg(pParse);
+          p = exprVectorField(pLeft, i);
+          pColl = sqlite3ExprCollSeq(pParse, p);
+
+          sqlite3VdbeAddOp3(v, OP_Column, iIdx, i, r2);
+          sqlite3VdbeAddOp4(v, OP_Eq, r1+i, 0, r2, (void*)pColl,P4_COLLSEQ);
+          sqlite3VdbeChangeP5(v, SQLITE_JUMPIFNULL);
+          sqlite3VdbeAddOp2(v, OP_Next, iIdx, addrNext);
+          sqlite3VdbeAddOp2(v, OP_Goto, 0, destIfFalse);
+          sqlite3VdbeJumpHere(v, sqlite3VdbeCurrentAddr(v)-3);
+          sqlite3ReleaseTempReg(pParse, r2);
+        }
+        sqlite3VdbeAddOp2(v, OP_Goto, 0, destIfNull);
+
+        /* The key was found in the index. If it contains any NULL values,
+        ** then the result of the IN(...) operator is NULL. Otherwise, the
+        ** result is 1.  */
+        sqlite3VdbeJumpHere(v, addr);
+        for(i=0; i<nVector; i++){
+          Expr *p = exprVectorField(pExpr->pLeft, i);
+          if( sqlite3ExprCanBeNull(p) ){
+            sqlite3VdbeAddOp2(v, OP_IsNull, r1+aiMap[i], destIfNull);
+          }
+        }
+
+      }else if( rRhsHasNull==0 ){
         /* This branch runs if it is known at compile time that the RHS
-        ** cannot contain NULL values. This happens as the result
-        ** of a "NOT NULL" constraint in the database schema.
+        ** cannot contain NULL values. This happens as a result
+        ** of "NOT NULL" constraints in the database schema.
         **
         ** Also run this branch if NULL is equivalent to FALSE
-        ** for this particular IN operator.
-        */
+        ** for this particular IN operator.  */
         sqlite3VdbeAddOp4Int(
             v, OP_NotFound, pExpr->iTable, destIfFalse, r1, nVector
         );
@@ -2571,7 +2609,7 @@ static void sqlite3ExprCodeIN(
         ** outcome.
         */
         int addr1;
-  
+
         /* First check to see if the LHS is contained in the RHS.  If so,
         ** then the answer is TRUE the presence of NULLs in the RHS does
         ** not matter.  If the LHS is not contained in the RHS, then the
@@ -3003,7 +3041,7 @@ int sqlite3ExprCodeTarget(Parse *pParse, Expr *pExpr, int target){
   int inReg = target;       /* Results stored in register inReg */
   int regFree1 = 0;         /* If non-zero free this temporary register */
   int regFree2 = 0;         /* If non-zero free this temporary register */
-  int r1, r2, r3, r4;       /* Various register numbers */
+  int r1, r2;               /* Various register numbers */
   sqlite3 *db = pParse->db; /* The database connection */
   Expr tempX;               /* Temporary expression node */
   int p5 = 0;
@@ -3404,30 +3442,6 @@ int sqlite3ExprCodeTarget(Parse *pParse, Expr *pExpr, int target){
     */
     case TK_BETWEEN: {
       exprCodeBetween(pParse, pExpr, target, 0, 0);
-#if 0
-      Expr *pLeft = pExpr->pLeft;
-      struct ExprList_item *pLItem = pExpr->x.pList->a;
-      Expr *pRight = pLItem->pExpr;
-
-      r1 = sqlite3ExprCodeTemp(pParse, pLeft, &regFree1);
-      r2 = sqlite3ExprCodeTemp(pParse, pRight, &regFree2);
-      testcase( regFree1==0 );
-      testcase( regFree2==0 );
-      r3 = sqlite3GetTempReg(pParse);
-      r4 = sqlite3GetTempReg(pParse);
-      codeCompare(pParse, pLeft, pRight, OP_Ge,
-                  r1, r2, r3, SQLITE_STOREP2);  VdbeCoverage(v);
-      pLItem++;
-      pRight = pLItem->pExpr;
-      sqlite3ReleaseTempReg(pParse, regFree2);
-      r2 = sqlite3ExprCodeTemp(pParse, pRight, &regFree2);
-      testcase( regFree2==0 );
-      codeCompare(pParse, pLeft, pRight, OP_Le, r1, r2, r4, SQLITE_STOREP2);
-      VdbeCoverage(v);
-      sqlite3VdbeAddOp3(v, OP_And, r3, r4, target);
-      sqlite3ReleaseTempReg(pParse, r3);
-      sqlite3ReleaseTempReg(pParse, r4);
-#endif
       break;
     }
     case TK_SPAN:
@@ -3908,7 +3922,7 @@ void sqlite3ExprIfTrue(Parse *pParse, Expr *pExpr, int dest, int jumpIfNull){
   if( NEVER(v==0) )     return;  /* Existence of VDBE checked by caller */
   if( NEVER(pExpr==0) ) return;  /* No way this can happen */
   op = pExpr->op;
-  switch( op | (pExpr->pLeft ? (pExpr->pLeft->flags & EP_Vector) : 0)){
+  switch( op ){
     case TK_AND: {
       int d2 = sqlite3VdbeMakeLabel(v);
       testcase( jumpIfNull==0 );
@@ -3945,6 +3959,7 @@ void sqlite3ExprIfTrue(Parse *pParse, Expr *pExpr, int dest, int jumpIfNull){
     case TK_GE:
     case TK_NE:
     case TK_EQ: {
+      if( pExpr->pLeft->flags & EP_Vector ) goto default_expr;
       testcase( jumpIfNull==0 );
       r1 = sqlite3ExprCodeTemp(pParse, pExpr->pLeft, &regFree1);
       r2 = sqlite3ExprCodeTemp(pParse, pExpr->pRight, &regFree2);
@@ -3991,6 +4006,7 @@ void sqlite3ExprIfTrue(Parse *pParse, Expr *pExpr, int dest, int jumpIfNull){
     }
 #endif
     default: {
+    default_expr:
       if( exprAlwaysTrue(pExpr) ){
         sqlite3VdbeGoto(v, dest);
       }else if( exprAlwaysFalse(pExpr) ){
index 67b2e67baadbe6af77c9188026b8c1a1833db506..9c9be99039fb4ce1f392c6886e709cb2ddce147f 100644 (file)
@@ -934,6 +934,7 @@ static void exprAnalyze(
   op = pExpr->op;
   if( op==TK_IN ){
     assert( pExpr->pRight==0 );
+    if( sqlite3ExprCheckIN(pParse, pExpr) ) return;
     if( ExprHasProperty(pExpr, EP_xIsSelect) ){
       pTerm->prereqRight = exprSelectUsage(pMaskSet, pExpr->x.pSelect);
     }else{
diff --git a/test/rowvalue4.test b/test/rowvalue4.test
new file mode 100644 (file)
index 0000000..1dfcfb9
--- /dev/null
@@ -0,0 +1,48 @@
+# 2016 July 29
+#
+# 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.  The
+# focus of this file is syntax errors involving row-value constructors
+# and sub-selects that return multiple arguments.
+#
+
+set testdir [file dirname $argv0]
+source $testdir/tester.tcl
+set ::testprefix rowvalue4
+
+do_execsql_test 0 {
+  CREATE TABLE t1(a, b, c);
+  CREATE INDEX t1bac ON t1(b, a, c);
+}
+
+foreach {tn e} {
+  1 "(1, 2, 3)"
+  2 "1 + (1, 2)"
+  3 "(1,2,3) == (1, 2)"
+} {
+  do_catchsql_test 1.$tn "SELECT $e" {1 {invalid use of row value}}
+}
+
+foreach {tn s error} {
+  1 "SELECT * FROM t1 WHERE a = (1, 2)"       {invalid use of row value}
+  2 "SELECT * FROM t1 WHERE b = (1, 2)"       {invalid use of row value}
+  3 "SELECT * FROM t1 WHERE NOT (b = (1, 2))" {invalid use of row value}
+  4 "SELECT * FROM t1 LIMIT (1, 2)"           {invalid use of row value}
+  5 "SELECT (a, b) IN (SELECT * FROM t1) FROM t1" 
+                             {sub-select returns 3 columns - expected 2}
+
+  6 "SELECT * FROM t1 WHERE (a, b) IN (SELECT * FROM t1)" 
+                             {sub-select returns 3 columns - expected 2}
+} {
+  do_catchsql_test 2.$tn "$s" [list 1 $error]
+}
+
+finish_test
+