]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add support for QUOTE and EXPLAIN modes.
authordrh <>
Thu, 23 Oct 2025 20:13:57 +0000 (20:13 +0000)
committerdrh <>
Thu, 23 Oct 2025 20:13:57 +0000 (20:13 +0000)
FossilOrigin-Name: c4d612a5720eace463ee8b1d7484eddd9701b8f2155479077a1410532e8d3e4e

ext/qrf/qrf-tester.c
ext/qrf/qrf.c
ext/qrf/qrf.h
manifest
manifest.uuid

index 6c57bb3ddd78dbca96564c5ad15c1d43329578fd..d264a7a7e9e8bdee5a507919bf72ba73beeb00be 100644 (file)
@@ -198,6 +198,7 @@ int main(int argc, char **argv){
          { "list",     QRF_MODE_List,     },
          { "markdown", QRF_MODE_Markdown, },
          { "off",      QRF_MODE_Off,      },
+         { "quote",    QRF_MODE_Quote,    },
          { "table",    QRF_MODE_Table,    },
          { "scanexp",  QRF_MODE_ScanExp,  },
       };
index 4ccc3e46f43b007a5e88a3051d0ec2da676c28a7..dbe1f05a65fe1c0b671c86500a2161e264d1377e 100644 (file)
@@ -15,6 +15,7 @@
 #include "qrf.h"
 #include <string.h>
 #include <ctype.h>
+#include <assert.h>
 
 typedef sqlite3_int64 i64;
 
@@ -47,10 +48,17 @@ struct Qrf {
   sqlite3_str *pOut;          /* Accumulated output */
   int iErr;                   /* Error code */
   int nCol;                   /* Number of output columns */
-  int mxColWth;               /* Maximum size of any column name */
   union {
-    const char **azCol;         /* Names of output columns (MODE_Line) */
-    EQPGraph *pGraph;           /* EQP graph (MODE_EQP) */
+    struct {                  /* Content for QRF_MODE_Line */
+      int mxColWth;             /* Maximum display width of any column */
+      const char **azCol;       /* Names of output columns (MODE_Line) */
+    } sLine;
+    EQPGraph *pGraph;         /* EQP graph (MODE_EQP) */
+    struct {                  /* Content for QRF_MODE_Explain */
+      int nIndent;              /* Slots allocated for aiIndent */
+      int iIndent;              /* Current slot */
+      int *aiIndent;            /* Indentation for each opcode */
+    } sExpln;
   } u;
   sqlite3_int64 nRow;         /* Number of rows handled so far */
   int *actualWidth;           /* Actual width of each column */
@@ -86,6 +94,8 @@ static void qrfOom(Qrf *p){
   qrfError(p, SQLITE_NOMEM, "out of memory");
 }
 
+
+
 /*
 ** Add a new entry to the EXPLAIN QUERY PLAN data
 */
@@ -1221,6 +1231,177 @@ qrf_column_end:
   return;
 }
 
+/*
+** Parameter azArray points to a zero-terminated array of strings. zStr
+** points to a single nul-terminated string. Return non-zero if zStr
+** is equal, according to strcmp(), to any of the strings in the array.
+** Otherwise, return zero.
+*/
+static int qrfStringInArray(const char *zStr, const char **azArray){
+  int i;
+  if( zStr==0 ) return 0;
+  for(i=0; azArray[i]; i++){
+    if( 0==strcmp(zStr, azArray[i]) ) return 1;
+  }
+  return 0;
+}
+
+/*
+** Print out an EXPLAIN with indentation.  This is a two-pass algorithm.
+**
+** On the first pass, we compute aiIndent[iOp] which is the amount of
+** indentation to apply to the iOp-th opcode.  The output actually occurs
+** on the second pass.
+**
+** The indenting rules are:
+**
+**     * For each "Next", "Prev", "VNext" or "VPrev" instruction, indent
+**       all opcodes that occur between the p2 jump destination and the opcode
+**       itself by 2 spaces.
+**
+**     * Do the previous for "Return" instructions for when P2 is positive.
+**       See tag-20220407a in wherecode.c and vdbe.c.
+**
+**     * For each "Goto", if the jump destination is earlier in the program
+**       and ends on one of:
+**          Yield  SeekGt  SeekLt  RowSetRead  Rewind
+**       or if the P1 parameter is one instead of zero,
+**       then indent all opcodes between the earlier instruction
+**       and "Goto" by 2 spaces.
+*/
+static void qrfExplain(Qrf *p){
+  int *abYield = 0;     /* abYield[iOp] is rue if opcode iOp is an OP_Yield */
+  int *aiIndent = 0;    /* Indent the iOp-th opcode by aiIndent[iOp] */
+  i64 nAlloc = 0;       /* Allocated size of aiIndent[], abYield */
+  int nIndent;          /* Number of entries in aiIndent[] */
+  int iOp;              /* Opcode number */
+  int i;                /* Column loop counter */
+
+  const char *azNext[] = { "Next", "Prev", "VPrev", "VNext", "SorterNext",
+                           "Return", 0 };
+  const char *azYield[] = { "Yield", "SeekLT", "SeekGT", "RowSetRead",
+                            "Rewind", 0 };
+  const char *azGoto[] = { "Goto", 0 };
+
+  /* The caller guarantees that the leftmost 4 columns of the statement
+  ** passed to this function are equivalent to the leftmost 4 columns
+  ** of EXPLAIN statement output. In practice the statement may be
+  ** an EXPLAIN, or it may be a query on the bytecode() virtual table.  */
+  assert( sqlite3_column_count(p->pStmt)>=4 );
+  assert( 0==sqlite3_stricmp( sqlite3_column_name(p->pStmt, 0), "addr" ) );
+  assert( 0==sqlite3_stricmp( sqlite3_column_name(p->pStmt, 1), "opcode" ) );
+  assert( 0==sqlite3_stricmp( sqlite3_column_name(p->pStmt, 2), "p1" ) );
+  assert( 0==sqlite3_stricmp( sqlite3_column_name(p->pStmt, 3), "p2" ) );
+
+  for(iOp=0; SQLITE_ROW==sqlite3_step(p->pStmt); iOp++){
+    int i;
+    int iAddr = sqlite3_column_int(p->pStmt, 0);
+    const char *zOp = (const char*)sqlite3_column_text(p->pStmt, 1);
+    int p1 = sqlite3_column_int(p->pStmt, 2);
+    int p2 = sqlite3_column_int(p->pStmt, 3);
+
+    /* Assuming that p2 is an instruction address, set variable p2op to the
+    ** index of that instruction in the aiIndent[] array. p2 and p2op may be
+    ** different if the current instruction is part of a sub-program generated
+    ** by an SQL trigger or foreign key.  */
+    int p2op = (p2 + (iOp-iAddr));
+
+    /* Grow the aiIndent array as required */
+    if( iOp>=nAlloc ){
+      nAlloc += 100;
+      aiIndent = (int*)sqlite3_realloc64(aiIndent, nAlloc*sizeof(int));
+      abYield = (int*)sqlite3_realloc64(abYield, nAlloc*sizeof(int));
+      if( aiIndent==0 || abYield==0 ){
+        qrfOom(p);
+        sqlite3_free(aiIndent);
+        sqlite3_free(abYield);
+        return;
+      }
+    }
+
+    abYield[iOp] = qrfStringInArray(zOp, azYield);
+    aiIndent[iOp] = 0;
+    nIndent = iOp+1;
+    if( qrfStringInArray(zOp, azNext) && p2op>0 ){
+      for(i=p2op; i<iOp; i++) aiIndent[i] += 2;
+    }
+    if( qrfStringInArray(zOp, azGoto) && p2op<iOp && (abYield[p2op] || p1) ){
+      for(i=p2op; i<iOp; i++) aiIndent[i] += 2;
+    }
+  }
+  sqlite3_free(abYield);
+
+  /* Second pass.  Actually generate output */
+  sqlite3_reset(p->pStmt);
+  if( p->iErr==SQLITE_OK ){
+    static const int aExplainWidth[] = {4,       13, 4, 4, 4, 13, 2, 13};
+    static const int aExplainMap[] =   {0,       1,  2, 3, 4, 5,  6, 7 };
+    static const int aScanExpWidth[] = {4,15, 6, 13, 4, 4, 4, 13, 2, 13};
+    static const int aScanExpMap[] =   {0, 9, 8, 1,  2, 3, 4, 5,  6, 7 };
+    const int *aWidth = aExplainWidth;
+    const int *aMap = aExplainMap;
+    int nWidth = sizeof(aExplainWidth)/sizeof(int);
+    int iIndent = 1;
+    int nArg = p->nCol;
+    if( p->spec.eFormat==QRF_MODE_ScanExp ){
+      aWidth = aScanExpWidth;
+      aMap = aScanExpMap;
+      nWidth = sizeof(aScanExpWidth)/sizeof(int);
+      iIndent = 3;
+    }
+    if( nArg>nWidth ) nArg = nWidth;
+
+    for(iOp=0; sqlite3_step(p->pStmt)==SQLITE_ROW; iOp++){
+      /* If this is the first row seen, print out the headers */
+      if( iOp==0 ){
+        for(i=0; i<nArg; i++){
+          const char *zCol = sqlite3_column_name(p->pStmt, aMap[i]);
+          qrfWidthPrint(p,p->pOut, aWidth[i], zCol);
+          if( i==nArg-1 ){
+            sqlite3_str_append(p->pOut, "\n", 1);
+          }else{
+            sqlite3_str_append(p->pOut, "  ", 2);
+          }
+        }
+        for(i=0; i<nArg; i++){
+          sqlite3_str_appendf(p->pOut, "%.*c", aWidth[i], '-');
+          if( i==nArg-1 ){
+            sqlite3_str_append(p->pOut, "\n", 1);
+          }else{
+            sqlite3_str_append(p->pOut, "  ", 2);
+          }
+        }
+      }
+  
+      for(i=0; i<nArg; i++){
+        const char *zSep = "  ";
+        int w = aWidth[i];
+        const char *zVal = sqlite3_column_text(p->pStmt, aMap[i]);
+        int len;
+        if( i==nArg-1 ) w = 0;
+        if( zVal==0 ) zVal = "";
+        len = qrfDisplayLength(zVal);
+        if( len>w ){
+          w = len;
+          zSep = " ";
+        }
+        if( i==iIndent && aiIndent && iOp<nIndent ){
+          sqlite3_str_appendchar(p->pOut, aiIndent[iOp], ' ');
+        }
+        qrfWidthPrint(p, p->pOut, w, zVal);
+        if( i==nArg-1 ){
+          sqlite3_str_append(p->pOut, "\n", 1);
+        }else{
+          sqlite3_str_appendall(p->pOut, zSep);
+        }
+      }
+      p->nRow++;
+    }
+    qrfWrite(p);
+  }
+  sqlite3_free(aiIndent);
+}
+
 /*
 ** Initialize the internal Qrf object.
 */
@@ -1291,6 +1472,13 @@ static void qrfInitialize(
       p->spec.zRowSep = "\r\n";
       break;
     }
+    case QRF_MODE_Quote: {
+      p->spec.eQuote = QRF_TXT_Sql;
+      p->spec.eBlob = QRF_BLOB_Sql;
+      p->spec.zColumnSep = ",";
+      p->spec.zRowSep = "\n";
+      break;
+    }
     case QRF_MODE_EQP: {
       if( sqlite3_stmt_isexplain(p->pStmt)!=2 ){
         /* If EQP mode is requested, but the statement is not an EXPLAIN QUERY
@@ -1299,12 +1487,29 @@ static void qrfInitialize(
         ** mode is EQP, so do not leave the mode in EQP if the statement is
         ** not an EQP statement.
         */
-        p->spec.eFormat = QRF_MODE_List;
+        p->spec.eFormat = QRF_MODE_Quote;
+        p->spec.bShowCNames = 1;
         p->spec.eQuote = QRF_TXT_Sql;
         p->spec.eBlob = QRF_BLOB_Sql;
         p->spec.zColumnSep = ",";
         p->spec.zRowSep = "\n";
+      }
+      break;
+    }
+    case QRF_MODE_Explain: {
+      if( sqlite3_stmt_isexplain(p->pStmt)!=1 ){
+        /* If Explain mode is requested, but the statement is not an EXPLAIN
+        ** tatement, then convert the mode to a comma-separate list of
+        ** SQL-quoted values.  Downstream expects an Explain statement if the
+        ** mode is Explain, so do not leave the mode in Explain if the
+        ** statement is not an EXPLAIN statement.
+        */
+        p->spec.eFormat = QRF_MODE_Quote;
         p->spec.bShowCNames = 1;
+        p->spec.eQuote = QRF_TXT_Sql;
+        p->spec.eBlob = QRF_BLOB_Sql;
+        p->spec.zColumnSep = ",";
+        p->spec.zRowSep = "\n";
       }
       break;
     }
@@ -1384,24 +1589,24 @@ static void qrfOneSimpleRow(Qrf *p){
       break;
     }
     case QRF_MODE_Line: {
-      if( p->u.azCol==0 ){
-        p->u.azCol = sqlite3_malloc64( p->nCol*sizeof(p->u.azCol[0]) );
-        if( p->u.azCol==0 ){
+      if( p->u.sLine.azCol==0 ){
+        p->u.sLine.azCol = sqlite3_malloc64( p->nCol*sizeof(char*) );
+        if( p->u.sLine.azCol==0 ){
           qrfOom(p);
           break;
         }
-        p->mxColWth = 0;
+        p->u.sLine.mxColWth = 0;
         for(i=0; i<p->nCol; i++){
           int sz;
-          p->u.azCol[i] = sqlite3_column_name(p->pStmt, i);
-          if( p->u.azCol[i]==0 ) p->u.azCol[i] = "unknown";
-          sz = qrfDisplayLength(p->u.azCol[i]);
-          if( sz > p->mxColWth ) p->mxColWth = sz;
+          p->u.sLine.azCol[i] = sqlite3_column_name(p->pStmt, i);
+          if( p->u.sLine.azCol[i]==0 ) p->u.sLine.azCol[i] = "unknown";
+          sz = qrfDisplayLength(p->u.sLine.azCol[i]);
+          if( sz > p->u.sLine.mxColWth ) p->u.sLine.mxColWth = sz;
         }
       }
       if( p->nRow ) sqlite3_str_append(p->pOut, "\n", 1);
       for(i=0; i<p->nCol; i++){
-        qrfWidthPrint(p, p->pOut, -p->mxColWth, p->u.azCol[i]);
+        qrfWidthPrint(p, p->pOut, -p->u.sLine.mxColWth, p->u.sLine.azCol[i]);
         sqlite3_str_append(p->pOut, " = ", 3);
         qrfRenderValue(p, p->pOut, i);
         if( i<p->nCol-1 ){
@@ -1460,7 +1665,7 @@ static void qrfFinalize(Qrf *p){
       break;
     }
     case QRF_MODE_Line: {
-      if( p->u.azCol ) sqlite3_free(p->u.azCol);
+      if( p->u.sLine.azCol ) sqlite3_free(p->u.sLine.azCol);
       break;
     }
     case QRF_MODE_EQP: {
@@ -1503,6 +1708,10 @@ int sqlite3_format_query_result(
       qrfColumnar(&qrf);
       break;
     }
+    case QRF_MODE_Explain: {
+      qrfExplain(&qrf);
+      break;
+    }
     default: {
       /* Non-columnar modes where the output can occur after each row
       ** of result is received */
index d40d02e24b779dd82d8c03f0cc7e5f9fc0914c54..8083660bb6d750fec1e5308bd40ddd126079d9c1 100644 (file)
@@ -68,15 +68,16 @@ int sqlite3_format_query_result(
 #define QRF_MODE_Json      3 /* Output is a list of JSON objects */
 #define QRF_MODE_Insert    4 /* Generate SQL "insert" statements */
 #define QRF_MODE_Csv       5 /* Comma-separated-value */
-#define QRF_MODE_Explain   6 /* EXPLAIN output */
-#define QRF_MODE_ScanExp   7 /* EXPLAIN output with vm stats */
-#define QRF_MODE_EQP       8 /* Format EXPLAIN QUERY PLAN output */
-#define QRF_MODE_Markdown  9 /* Markdown formatting */
-#define QRF_MODE_Column   10 /* One record per line in neat columns */
-#define QRF_MODE_Table    11 /* MySQL-style table formatting */
-#define QRF_MODE_Box      12 /* Unicode box-drawing characters */
-#define QRF_MODE_Count    13 /* Output only a count of the rows of output */
-#define QRF_MODE_Off      14 /* No query output shown */
+#define QRF_MODE_Quote     6 /* SQL-quoted, comma-separated */
+#define QRF_MODE_Explain   7 /* EXPLAIN output */
+#define QRF_MODE_ScanExp   8 /* EXPLAIN output with vm stats */
+#define QRF_MODE_EQP       9 /* Format EXPLAIN QUERY PLAN output */
+#define QRF_MODE_Markdown 10 /* Markdown formatting */
+#define QRF_MODE_Column   11 /* One record per line in neat columns */
+#define QRF_MODE_Table    12 /* MySQL-style table formatting */
+#define QRF_MODE_Box      13 /* Unicode box-drawing characters */
+#define QRF_MODE_Count    14 /* Output only a count of the rows of output */
+#define QRF_MODE_Off      15 /* No query output shown */
 
 /*
 ** Quoting styles for text.
index 0afdf62cc5051597689bee089ba493c99c8a782f..ce15f6ce19f3eb12222d8987078d7c126dd0fbae 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Add\ssupport\sfor\sEQP\smode.
-D 2025-10-23T18:57:15.892
+C Add\ssupport\sfor\sQUOTE\sand\sEXPLAIN\smodes.
+D 2025-10-23T20:13:57.000
 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -416,9 +416,9 @@ F ext/misc/wholenumber.c 0fa0c082676b7868bf2fa918e911133f2b349bcdceabd1198bba5f6
 F ext/misc/windirent.h 02211ce51f3034c675f2dbf4d228194d51b3ee05734678bad5106fff6292e60c
 F ext/misc/zipfile.c 09e6e3a3ff40a99677de3c0bc6569bd5f4709b1844ac3d1c1452a456c5a62f1c
 F ext/misc/zorder.c bddff2e1b9661a90c95c2a9a9c7ecd8908afab5763256294dd12d609d4664eee
-F ext/qrf/qrf-tester.c b35585d96e36cdefa0dcef5716830d0bac123b0c776e27622744216c16d213da
-F ext/qrf/qrf.c d1956c9cf3c2a6341a511602281bd43a2c694512ba9c1c2ed9f22ce6a4e03eda
-F ext/qrf/qrf.h 8b4a6ee36c38778fc9cec6f34b555b3d33cc4d388babade6de0f64cda6a24d73
+F ext/qrf/qrf-tester.c 7d72e3268db31a348c2e8002b174eac9a25d1f93350d845e1ef5f60dfddd95a9
+F ext/qrf/qrf.c 2c262b394f7da3ff08af74e6b740d4517f3dbac1184b873eeba3228c8564fe47
+F ext/qrf/qrf.h 525fe1161fec27106d8affa6f1e4ead64cadbc9d5e7debecaf1bad5a23594673
 F ext/qrf/qrf.md 4eea619191dab7bbf483eff3fe3b074a07d7c8c50bc86070a4485797d386d1ff
 F ext/rbu/rbu.c 801450b24eaf14440d8fd20385aacc751d5c9d6123398df41b1b5aa804bf4ce8
 F ext/rbu/rbu1.test 25870dd7db7eb5597e2b4d6e29e7a7e095abf332660f67d89959552ce8f8f255
@@ -2175,8 +2175,8 @@ F tool/version-info.c 33d0390ef484b3b1cb685d59362be891ea162123cea181cb8e6d2cf6dd
 F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7
 F tool/warnings.sh 1ad0169b022b280bcaaf94a7fa231591be96b514230ab5c98fbf15cd7df842dd
 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
-P bf0fec0d4961bd4df3ff874bf8d32b28df48a1739e107ffb0b7bf57affe5f0b6
-R 83abd8eeaf8a5c4adee9e866c94bc5e8
+P 61b2011d8ec2bfe98595ac9ccae5ed0037444757a800706d0ecbc279014fd3fb
+R 300fe5b838870830daae1638e8fbd758
 U drh
-Z e9931b276084795d788f130b069357cb
+Z 500f02b47e428273d17848f35caac609
 # Remove this line to create a well-formed Fossil manifest.
index 3790e7cab39ea1ba1ad66fecf6c2c45138c60109..d74fa129d2aa23d45fc874e4e2fab38f36c2c36b 100644 (file)
@@ -1 +1 @@
-61b2011d8ec2bfe98595ac9ccae5ed0037444757a800706d0ecbc279014fd3fb
+c4d612a5720eace463ee8b1d7484eddd9701b8f2155479077a1410532e8d3e4e