--- /dev/null
- #include <ctype.h>
+/*
+** 2025-10-20
+**
+** 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.
+**
+*************************************************************************
+** Implementation of the Result-Format or "qrf" utility library for SQLite.
+** See the qrf.md documentation for additional information.
+*/
+#ifndef SQLITE_QRF_H
+#include "qrf.h"
+#endif
+#include <string.h>
- if( bWrap && z[i]!=0 && !isspace(z[i]) && isalnum(c)==isalnum(z[i]) ){
+#include <assert.h>
+
+typedef sqlite3_int64 i64;
+
+/* A single line in the EQP output */
+typedef struct qrfEQPGraphRow qrfEQPGraphRow;
+struct qrfEQPGraphRow {
+ int iEqpId; /* ID for this row */
+ int iParentId; /* ID of the parent row */
+ qrfEQPGraphRow *pNext; /* Next row in sequence */
+ char zText[1]; /* Text to display for this row */
+};
+
+/* All EQP output is collected into an instance of the following */
+typedef struct qrfEQPGraph qrfEQPGraph;
+struct qrfEQPGraph {
+ qrfEQPGraphRow *pRow; /* Linked list of all rows of the EQP output */
+ qrfEQPGraphRow *pLast; /* Last element of the pRow list */
+ char zPrefix[100]; /* Graph prefix */
+};
+
+/*
+** Private state information. Subject to change from one release to the
+** next.
+*/
+typedef struct Qrf Qrf;
+struct Qrf {
+ sqlite3_stmt *pStmt; /* The statement whose output is to be rendered */
+ sqlite3 *db; /* The corresponding database connection */
+ sqlite3_stmt *pJTrans; /* JSONB to JSON translator statement */
+ char **pzErr; /* Write error message here, if not NULL */
+ sqlite3_str *pOut; /* Accumulated output */
+ int iErr; /* Error code */
+ int nCol; /* Number of output columns */
+ int expMode; /* Original sqlite3_stmt_isexplain() plus 1 */
+ int mxWidth; /* Screen width */
+ int mxHeight; /* nLineLimit */
+ union {
+ struct { /* Content for QRF_STYLE_Line */
+ int mxColWth; /* Maximum display width of any column */
+ const char **azCol; /* Names of output columns (MODE_Line) */
+ } sLine;
+ qrfEQPGraph *pGraph; /* EQP graph (Eqp, Stats, and StatsEst) */
+ struct { /* Content for QRF_STYLE_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 */
+ sqlite3_qrf_spec spec; /* Copy of the original spec */
+};
+
++/*
++** Data for substitute ctype.h functions. Used for x-platform
++** consistency and so that '_' is counted as an alphabetic
++** character.
++**
++** 0x01 - space
++** 0x02 - digit
++** 0x04 - alphabetic, including '_'
++*/
++static const char qrfCType[] = {
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0,
++ 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
++ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 4,
++ 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
++ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
++ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
++};
++#define qrfSpace(x) ((qrfCType[(unsigned char)x]&1)!=0)
++#define qrfDigit(x) ((qrfCType[(unsigned char)x]&2)!=0)
++#define qrfAlpha(x) ((qrfCType[(unsigned char)x]&4)!=0)
++#define qrfAlnum(x) ((qrfCType[(unsigned char)x]&6)!=0)
++
+/*
+** Set an error code and error message.
+*/
+static void qrfError(
+ Qrf *p, /* Query result state */
+ int iCode, /* Error code */
+ const char *zFormat, /* Message format (or NULL) */
+ ...
+){
+ p->iErr = iCode;
+ if( p->pzErr!=0 ){
+ sqlite3_free(*p->pzErr);
+ *p->pzErr = 0;
+ if( zFormat ){
+ va_list ap;
+ va_start(ap, zFormat);
+ *p->pzErr = sqlite3_vmprintf(zFormat, ap);
+ va_end(ap);
+ }
+ }
+}
+
+/*
+** Out-of-memory error.
+*/
+static void qrfOom(Qrf *p){
+ qrfError(p, SQLITE_NOMEM, "out of memory");
+}
+
+
+
+/*
+** Add a new entry to the EXPLAIN QUERY PLAN data
+*/
+static void qrfEqpAppend(Qrf *p, int iEqpId, int p2, const char *zText){
+ qrfEQPGraphRow *pNew;
+ sqlite3_int64 nText;
+ if( zText==0 ) return;
+ if( p->u.pGraph==0 ){
+ p->u.pGraph = sqlite3_malloc64( sizeof(qrfEQPGraph) );
+ if( p->u.pGraph==0 ){
+ qrfOom(p);
+ return;
+ }
+ memset(p->u.pGraph, 0, sizeof(qrfEQPGraph) );
+ }
+ nText = strlen(zText);
+ pNew = sqlite3_malloc64( sizeof(*pNew) + nText );
+ if( pNew==0 ){
+ qrfOom(p);
+ return;
+ }
+ pNew->iEqpId = iEqpId;
+ pNew->iParentId = p2;
+ memcpy(pNew->zText, zText, nText+1);
+ pNew->pNext = 0;
+ if( p->u.pGraph->pLast ){
+ p->u.pGraph->pLast->pNext = pNew;
+ }else{
+ p->u.pGraph->pRow = pNew;
+ }
+ p->u.pGraph->pLast = pNew;
+}
+
+/*
+** Free and reset the EXPLAIN QUERY PLAN data that has been collected
+** in p->u.pGraph.
+*/
+static void qrfEqpReset(Qrf *p){
+ qrfEQPGraphRow *pRow, *pNext;
+ if( p->u.pGraph ){
+ for(pRow = p->u.pGraph->pRow; pRow; pRow = pNext){
+ pNext = pRow->pNext;
+ sqlite3_free(pRow);
+ }
+ sqlite3_free(p->u.pGraph);
+ p->u.pGraph = 0;
+ }
+}
+
+/* Return the next EXPLAIN QUERY PLAN line with iEqpId that occurs after
+** pOld, or return the first such line if pOld is NULL
+*/
+static qrfEQPGraphRow *qrfEqpNextRow(Qrf *p, int iEqpId, qrfEQPGraphRow *pOld){
+ qrfEQPGraphRow *pRow = pOld ? pOld->pNext : p->u.pGraph->pRow;
+ while( pRow && pRow->iParentId!=iEqpId ) pRow = pRow->pNext;
+ return pRow;
+}
+
+/* Render a single level of the graph that has iEqpId as its parent. Called
+** recursively to render sublevels.
+*/
+static void qrfEqpRenderLevel(Qrf *p, int iEqpId){
+ qrfEQPGraphRow *pRow, *pNext;
+ i64 n = strlen(p->u.pGraph->zPrefix);
+ char *z;
+ for(pRow = qrfEqpNextRow(p, iEqpId, 0); pRow; pRow = pNext){
+ pNext = qrfEqpNextRow(p, iEqpId, pRow);
+ z = pRow->zText;
+ sqlite3_str_appendf(p->pOut, "%s%s%s\n", p->u.pGraph->zPrefix,
+ pNext ? "|--" : "`--", z);
+ if( n<(i64)sizeof(p->u.pGraph->zPrefix)-7 ){
+ memcpy(&p->u.pGraph->zPrefix[n], pNext ? "| " : " ", 4);
+ qrfEqpRenderLevel(p, pRow->iEqpId);
+ p->u.pGraph->zPrefix[n] = 0;
+ }
+ }
+}
+
+/*
+** Display and reset the EXPLAIN QUERY PLAN data
+*/
+static void qrfEqpRender(Qrf *p, i64 nCycle){
+ qrfEQPGraphRow *pRow;
+ if( p->u.pGraph!=0 && (pRow = p->u.pGraph->pRow)!=0 ){
+ if( pRow->zText[0]=='-' ){
+ if( pRow->pNext==0 ){
+ qrfEqpReset(p);
+ return;
+ }
+ sqlite3_str_appendf(p->pOut, "%s\n", pRow->zText+3);
+ p->u.pGraph->pRow = pRow->pNext;
+ sqlite3_free(pRow);
+ }else if( nCycle>0 ){
+ sqlite3_str_appendf(p->pOut, "QUERY PLAN (cycles=%lld [100%%])\n",nCycle);
+ }else{
+ sqlite3_str_appendall(p->pOut, "QUERY PLAN\n");
+ }
+ p->u.pGraph->zPrefix[0] = 0;
+ qrfEqpRenderLevel(p, 0);
+ qrfEqpReset(p);
+ }
+}
+
+#ifdef SQLITE_ENABLE_STMT_SCANSTATUS
+/*
+** Helper function for qrfExpStats().
+**
+*/
+static int qrfStatsHeight(sqlite3_stmt *p, int iEntry){
+ int iPid = 0;
+ int ret = 1;
+ sqlite3_stmt_scanstatus_v2(p, iEntry,
+ SQLITE_SCANSTAT_SELECTID, SQLITE_SCANSTAT_COMPLEX, (void*)&iPid
+ );
+ while( iPid!=0 ){
+ int ii;
+ for(ii=0; 1; ii++){
+ int iId;
+ int res;
+ res = sqlite3_stmt_scanstatus_v2(p, ii,
+ SQLITE_SCANSTAT_SELECTID, SQLITE_SCANSTAT_COMPLEX, (void*)&iId
+ );
+ if( res ) break;
+ if( iId==iPid ){
+ sqlite3_stmt_scanstatus_v2(p, ii,
+ SQLITE_SCANSTAT_PARENTID, SQLITE_SCANSTAT_COMPLEX, (void*)&iPid
+ );
+ }
+ }
+ ret++;
+ }
+ return ret;
+}
+#endif /* SQLITE_ENABLE_STMT_SCANSTATUS */
+
+
+/*
+** Generate ".scanstatus est" style of EQP output.
+*/
+static void qrfEqpStats(Qrf *p){
+#ifndef SQLITE_ENABLE_STMT_SCANSTATUS
+ qrfError(p, SQLITE_ERROR, "not available in this build");
+#else
+ static const int f = SQLITE_SCANSTAT_COMPLEX;
+ sqlite3_stmt *pS = p->pStmt;
+ int i = 0;
+ i64 nTotal = 0;
+ int nWidth = 0;
+ sqlite3_str *pLine = sqlite3_str_new(p->db);
+ sqlite3_str *pStats = sqlite3_str_new(p->db);
+ qrfEqpReset(p);
+
+ for(i=0; 1; i++){
+ const char *z = 0;
+ int n = 0;
+ if( sqlite3_stmt_scanstatus_v2(pS,i,SQLITE_SCANSTAT_EXPLAIN,f,(void*)&z) ){
+ break;
+ }
+ n = (int)strlen(z) + qrfStatsHeight(pS,i)*3;
+ if( n>nWidth ) nWidth = n;
+ }
+ nWidth += 4;
+
+ sqlite3_stmt_scanstatus_v2(pS,-1, SQLITE_SCANSTAT_NCYCLE, f, (void*)&nTotal);
+ for(i=0; 1; i++){
+ i64 nLoop = 0;
+ i64 nRow = 0;
+ i64 nCycle = 0;
+ int iId = 0;
+ int iPid = 0;
+ const char *zo = 0;
+ const char *zName = 0;
+ double rEst = 0.0;
+
+ if( sqlite3_stmt_scanstatus_v2(pS,i,SQLITE_SCANSTAT_EXPLAIN,f,(void*)&zo) ){
+ break;
+ }
+ sqlite3_stmt_scanstatus_v2(pS,i, SQLITE_SCANSTAT_EST,f,(void*)&rEst);
+ sqlite3_stmt_scanstatus_v2(pS,i, SQLITE_SCANSTAT_NLOOP,f,(void*)&nLoop);
+ sqlite3_stmt_scanstatus_v2(pS,i, SQLITE_SCANSTAT_NVISIT,f,(void*)&nRow);
+ sqlite3_stmt_scanstatus_v2(pS,i, SQLITE_SCANSTAT_NCYCLE,f,(void*)&nCycle);
+ sqlite3_stmt_scanstatus_v2(pS,i, SQLITE_SCANSTAT_SELECTID,f,(void*)&iId);
+ sqlite3_stmt_scanstatus_v2(pS,i, SQLITE_SCANSTAT_PARENTID,f,(void*)&iPid);
+ sqlite3_stmt_scanstatus_v2(pS,i, SQLITE_SCANSTAT_NAME,f,(void*)&zName);
+
+ if( nCycle>=0 || nLoop>=0 || nRow>=0 ){
+ const char *zSp = "";
+ double rpl;
+ sqlite3_str_reset(pStats);
+ if( nCycle>=0 && nTotal>0 ){
+ sqlite3_str_appendf(pStats, "cycles=%lld [%d%%]",
+ nCycle, ((nCycle*100)+nTotal/2) / nTotal
+ );
+ zSp = " ";
+ }
+ if( nLoop>=0 ){
+ sqlite3_str_appendf(pStats, "%sloops=%lld", zSp, nLoop);
+ zSp = " ";
+ }
+ if( nRow>=0 ){
+ sqlite3_str_appendf(pStats, "%srows=%lld", zSp, nRow);
+ zSp = " ";
+ }
+
+ if( p->spec.eStyle==QRF_STYLE_StatsEst ){
+ rpl = (double)nRow / (double)nLoop;
+ sqlite3_str_appendf(pStats, "%srpl=%.1f est=%.1f", zSp, rpl, rEst);
+ }
+
+ sqlite3_str_appendf(pLine,
+ "% *s (%s)", -1*(nWidth-qrfStatsHeight(pS,i)*3), zo,
+ sqlite3_str_value(pStats)
+ );
+ sqlite3_str_reset(pStats);
+ qrfEqpAppend(p, iId, iPid, sqlite3_str_value(pLine));
+ sqlite3_str_reset(pLine);
+ }else{
+ qrfEqpAppend(p, iId, iPid, zo);
+ }
+ }
+ sqlite3_free(sqlite3_str_finish(pLine));
+ sqlite3_free(sqlite3_str_finish(pStats));
+#endif
+}
+
+
+/*
+** Reset the prepared statement.
+*/
+static void qrfResetStmt(Qrf *p){
+ int rc = sqlite3_reset(p->pStmt);
+ if( rc!=SQLITE_OK && p->iErr==SQLITE_OK ){
+ qrfError(p, rc, "%s", sqlite3_errmsg(p->db));
+ }
+}
+
+/*
+** If xWrite is defined, send all content of pOut to xWrite and
+** reset pOut.
+*/
+static void qrfWrite(Qrf *p){
+ int n;
+ if( p->spec.xWrite && (n = sqlite3_str_length(p->pOut))>0 ){
+ int rc = p->spec.xWrite(p->spec.pWriteArg,
+ sqlite3_str_value(p->pOut),
+ (sqlite3_int64)n);
+ sqlite3_str_reset(p->pOut);
+ if( rc ){
+ qrfError(p, rc, "Failed to write %d bytes of output", n);
+ }
+ }
+}
+
+/* Lookup table to estimate the number of columns consumed by a Unicode
+** character.
+*/
+static const struct {
+ unsigned char w; /* Width of the character in columns */
+ int iFirst; /* First character in a span having this width */
+} aQrfUWidth[] = {
+ /* {1, 0x00000}, */
+ {0, 0x00300}, {1, 0x00370}, {0, 0x00483}, {1, 0x00487}, {0, 0x00488},
+ {1, 0x0048a}, {0, 0x00591}, {1, 0x005be}, {0, 0x005bf}, {1, 0x005c0},
+ {0, 0x005c1}, {1, 0x005c3}, {0, 0x005c4}, {1, 0x005c6}, {0, 0x005c7},
+ {1, 0x005c8}, {0, 0x00600}, {1, 0x00604}, {0, 0x00610}, {1, 0x00616},
+ {0, 0x0064b}, {1, 0x0065f}, {0, 0x00670}, {1, 0x00671}, {0, 0x006d6},
+ {1, 0x006e5}, {0, 0x006e7}, {1, 0x006e9}, {0, 0x006ea}, {1, 0x006ee},
+ {0, 0x0070f}, {1, 0x00710}, {0, 0x00711}, {1, 0x00712}, {0, 0x00730},
+ {1, 0x0074b}, {0, 0x007a6}, {1, 0x007b1}, {0, 0x007eb}, {1, 0x007f4},
+ {0, 0x00901}, {1, 0x00903}, {0, 0x0093c}, {1, 0x0093d}, {0, 0x00941},
+ {1, 0x00949}, {0, 0x0094d}, {1, 0x0094e}, {0, 0x00951}, {1, 0x00955},
+ {0, 0x00962}, {1, 0x00964}, {0, 0x00981}, {1, 0x00982}, {0, 0x009bc},
+ {1, 0x009bd}, {0, 0x009c1}, {1, 0x009c5}, {0, 0x009cd}, {1, 0x009ce},
+ {0, 0x009e2}, {1, 0x009e4}, {0, 0x00a01}, {1, 0x00a03}, {0, 0x00a3c},
+ {1, 0x00a3d}, {0, 0x00a41}, {1, 0x00a43}, {0, 0x00a47}, {1, 0x00a49},
+ {0, 0x00a4b}, {1, 0x00a4e}, {0, 0x00a70}, {1, 0x00a72}, {0, 0x00a81},
+ {1, 0x00a83}, {0, 0x00abc}, {1, 0x00abd}, {0, 0x00ac1}, {1, 0x00ac6},
+ {0, 0x00ac7}, {1, 0x00ac9}, {0, 0x00acd}, {1, 0x00ace}, {0, 0x00ae2},
+ {1, 0x00ae4}, {0, 0x00b01}, {1, 0x00b02}, {0, 0x00b3c}, {1, 0x00b3d},
+ {0, 0x00b3f}, {1, 0x00b40}, {0, 0x00b41}, {1, 0x00b44}, {0, 0x00b4d},
+ {1, 0x00b4e}, {0, 0x00b56}, {1, 0x00b57}, {0, 0x00b82}, {1, 0x00b83},
+ {0, 0x00bc0}, {1, 0x00bc1}, {0, 0x00bcd}, {1, 0x00bce}, {0, 0x00c3e},
+ {1, 0x00c41}, {0, 0x00c46}, {1, 0x00c49}, {0, 0x00c4a}, {1, 0x00c4e},
+ {0, 0x00c55}, {1, 0x00c57}, {0, 0x00cbc}, {1, 0x00cbd}, {0, 0x00cbf},
+ {1, 0x00cc0}, {0, 0x00cc6}, {1, 0x00cc7}, {0, 0x00ccc}, {1, 0x00cce},
+ {0, 0x00ce2}, {1, 0x00ce4}, {0, 0x00d41}, {1, 0x00d44}, {0, 0x00d4d},
+ {1, 0x00d4e}, {0, 0x00dca}, {1, 0x00dcb}, {0, 0x00dd2}, {1, 0x00dd5},
+ {0, 0x00dd6}, {1, 0x00dd7}, {0, 0x00e31}, {1, 0x00e32}, {0, 0x00e34},
+ {1, 0x00e3b}, {0, 0x00e47}, {1, 0x00e4f}, {0, 0x00eb1}, {1, 0x00eb2},
+ {0, 0x00eb4}, {1, 0x00eba}, {0, 0x00ebb}, {1, 0x00ebd}, {0, 0x00ec8},
+ {1, 0x00ece}, {0, 0x00f18}, {1, 0x00f1a}, {0, 0x00f35}, {1, 0x00f36},
+ {0, 0x00f37}, {1, 0x00f38}, {0, 0x00f39}, {1, 0x00f3a}, {0, 0x00f71},
+ {1, 0x00f7f}, {0, 0x00f80}, {1, 0x00f85}, {0, 0x00f86}, {1, 0x00f88},
+ {0, 0x00f90}, {1, 0x00f98}, {0, 0x00f99}, {1, 0x00fbd}, {0, 0x00fc6},
+ {1, 0x00fc7}, {0, 0x0102d}, {1, 0x01031}, {0, 0x01032}, {1, 0x01033},
+ {0, 0x01036}, {1, 0x01038}, {0, 0x01039}, {1, 0x0103a}, {0, 0x01058},
+ {1, 0x0105a}, {2, 0x01100}, {0, 0x01160}, {1, 0x01200}, {0, 0x0135f},
+ {1, 0x01360}, {0, 0x01712}, {1, 0x01715}, {0, 0x01732}, {1, 0x01735},
+ {0, 0x01752}, {1, 0x01754}, {0, 0x01772}, {1, 0x01774}, {0, 0x017b4},
+ {1, 0x017b6}, {0, 0x017b7}, {1, 0x017be}, {0, 0x017c6}, {1, 0x017c7},
+ {0, 0x017c9}, {1, 0x017d4}, {0, 0x017dd}, {1, 0x017de}, {0, 0x0180b},
+ {1, 0x0180e}, {0, 0x018a9}, {1, 0x018aa}, {0, 0x01920}, {1, 0x01923},
+ {0, 0x01927}, {1, 0x01929}, {0, 0x01932}, {1, 0x01933}, {0, 0x01939},
+ {1, 0x0193c}, {0, 0x01a17}, {1, 0x01a19}, {0, 0x01b00}, {1, 0x01b04},
+ {0, 0x01b34}, {1, 0x01b35}, {0, 0x01b36}, {1, 0x01b3b}, {0, 0x01b3c},
+ {1, 0x01b3d}, {0, 0x01b42}, {1, 0x01b43}, {0, 0x01b6b}, {1, 0x01b74},
+ {0, 0x01dc0}, {1, 0x01dcb}, {0, 0x01dfe}, {1, 0x01e00}, {0, 0x0200b},
+ {1, 0x02010}, {0, 0x0202a}, {1, 0x0202f}, {0, 0x02060}, {1, 0x02064},
+ {0, 0x0206a}, {1, 0x02070}, {0, 0x020d0}, {1, 0x020f0}, {2, 0x02329},
+ {1, 0x0232b}, {2, 0x02e80}, {0, 0x0302a}, {2, 0x03030}, {1, 0x0303f},
+ {2, 0x03040}, {0, 0x03099}, {2, 0x0309b}, {1, 0x0a4d0}, {0, 0x0a806},
+ {1, 0x0a807}, {0, 0x0a80b}, {1, 0x0a80c}, {0, 0x0a825}, {1, 0x0a827},
+ {2, 0x0ac00}, {1, 0x0d7a4}, {2, 0x0f900}, {1, 0x0fb00}, {0, 0x0fb1e},
+ {1, 0x0fb1f}, {0, 0x0fe00}, {2, 0x0fe10}, {1, 0x0fe1a}, {0, 0x0fe20},
+ {1, 0x0fe24}, {2, 0x0fe30}, {1, 0x0fe70}, {0, 0x0feff}, {2, 0x0ff00},
+ {1, 0x0ff61}, {2, 0x0ffe0}, {1, 0x0ffe7}, {0, 0x0fff9}, {1, 0x0fffc},
+ {0, 0x10a01}, {1, 0x10a04}, {0, 0x10a05}, {1, 0x10a07}, {0, 0x10a0c},
+ {1, 0x10a10}, {0, 0x10a38}, {1, 0x10a3b}, {0, 0x10a3f}, {1, 0x10a40},
+ {0, 0x1d167}, {1, 0x1d16a}, {0, 0x1d173}, {1, 0x1d183}, {0, 0x1d185},
+ {1, 0x1d18c}, {0, 0x1d1aa}, {1, 0x1d1ae}, {0, 0x1d242}, {1, 0x1d245},
+ {2, 0x20000}, {1, 0x2fffe}, {2, 0x30000}, {1, 0x3fffe}, {0, 0xe0001},
+ {1, 0xe0002}, {0, 0xe0020}, {1, 0xe0080}, {0, 0xe0100}, {1, 0xe01f0}
+};
+
+/*
+** Return an estimate of the width, in columns, for the single Unicode
+** character c. For normal characters, the answer is always 1. But the
+** estimate might be 0 or 2 for zero-width and double-width characters.
+**
+** Different display devices display unicode using different widths. So
+** it is impossible to know that true display width with 100% accuracy.
+** Inaccuracies in the width estimates might cause columns to be misaligned.
+** Unfortunately, there is nothing we can do about that.
+*/
+int sqlite3_qrf_wcwidth(int c){
+ int iFirst, iLast;
+
+ /* Fast path for common characters */
+ if( c<=0x300 ) return 1;
+
+ /* The general case */
+ iFirst = 0;
+ iLast = sizeof(aQrfUWidth)/sizeof(aQrfUWidth[0]) - 1;
+ while( iFirst<iLast-1 ){
+ int iMid = (iFirst+iLast)/2;
+ int cMid = aQrfUWidth[iMid].iFirst;
+ if( cMid < c ){
+ iFirst = iMid;
+ }else if( cMid > c ){
+ iLast = iMid - 1;
+ }else{
+ return aQrfUWidth[iMid].w;
+ }
+ }
+ if( aQrfUWidth[iLast].iFirst > c ) return aQrfUWidth[iFirst].w;
+ return aQrfUWidth[iLast].w;
+}
+
+/*
+** Compute the value and length of a multi-byte UTF-8 character that
+** begins at z[0]. Return the length. Write the Unicode value into *pU.
+**
+** This routine only works for *multi-byte* UTF-8 characters. It does
+** not attempt to detect illegal characters.
+*/
+int sqlite3_qrf_decode_utf8(const unsigned char *z, int *pU){
+ if( (z[0] & 0xe0)==0xc0 && (z[1] & 0xc0)==0x80 ){
+ *pU = ((z[0] & 0x1f)<<6) | (z[1] & 0x3f);
+ return 2;
+ }
+ if( (z[0] & 0xf0)==0xe0 && (z[1] & 0xc0)==0x80 && (z[2] & 0xc0)==0x80 ){
+ *pU = ((z[0] & 0x0f)<<12) | ((z[1] & 0x3f)<<6) | (z[2] & 0x3f);
+ return 3;
+ }
+ if( (z[0] & 0xf8)==0xf0 && (z[1] & 0xc0)==0x80 && (z[2] & 0xc0)==0x80
+ && (z[3] & 0xc0)==0x80
+ ){
+ *pU = ((z[0] & 0x0f)<<18) | ((z[1] & 0x3f)<<12) | ((z[2] & 0x3f))<<6
+ | (z[3] & 0x3f);
+ return 4;
+ }
+ *pU = 0;
+ return 1;
+}
+
+/*
+** Check to see if z[] is a valid VT100 escape. If it is, then
+** return the number of bytes in the escape sequence. Return 0 if
+** z[] is not a VT100 escape.
+**
+** This routine assumes that z[0] is \033 (ESC).
+*/
+static int qrfIsVt100(const unsigned char *z){
+ int i;
+ if( z[1]!='[' ) return 0;
+ i = 2;
+ while( z[i]>=0x30 && z[i]<=0x3f ){ i++; }
+ while( z[i]>=0x20 && z[i]<=0x2f ){ i++; }
+ if( z[i]<0x40 || z[i]>0x7e ) return 0;
+ return i+1;
+}
+
+/*
+** Return the length of a string in display characters.
+** Multibyte UTF8 characters count as a single character
+** for single-width characters, or as two characters for
+** double-width characters.
+*/
+static int qrfDisplayLength(const char *zIn){
+ const unsigned char *z = (const unsigned char*)zIn;
+ int n = 0;
+ while( *z ){
+ if( z[0]<' ' ){
+ int k;
+ if( z[0]=='\033' && (k = qrfIsVt100(z))>0 ){
+ z += k;
+ }else{
+ z++;
+ }
+ }else if( (0x80&z[0])==0 ){
+ n++;
+ z++;
+ }else{
+ int u = 0;
+ int len = sqlite3_qrf_decode_utf8(z, &u);
+ z += len;
+ n += sqlite3_qrf_wcwidth(u);
+ }
+ }
+ return n;
+}
+
+/*
+** Return the display width of the longest line of text
+** in the (possibly) multi-line input string zIn[0..nByte].
+** zIn[] is not necessarily zero-terminated. Take
+** into account tab characters, zero- and double-width
+** characters, CR and NL, and VT100 escape codes.
+**
+** Write the number of newlines into *pnNL. So, *pnNL will
+** return 0 if everything fits on one line, or positive it
+** it will need to be split.
+*/
+static int qrfDisplayWidth(const char *zIn, sqlite3_int64 nByte, int *pnNL){
+ const unsigned char *z = (const unsigned char*)zIn;
+ const unsigned char *zEnd = &z[nByte];
+ int mx = 0;
+ int n = 0;
+ int nNL = 0;
+ while( z<zEnd ){
+ if( z[0]<' ' ){
+ int k;
+ if( z[0]=='\033' && (k = qrfIsVt100(z))>0 ){
+ z += k;
+ }else{
+ if( z[0]=='\t' ){
+ n = (n+8)&~7;
+ }else if( z[0]=='\n' || z[0]=='\r' ){
+ nNL++;
+ if( n>mx ) mx = n;
+ n = 0;
+ }
+ z++;
+ }
+ }else if( (0x80&z[0])==0 ){
+ n++;
+ z++;
+ }else{
+ int u = 0;
+ int len = sqlite3_qrf_decode_utf8(z, &u);
+ z += len;
+ n += sqlite3_qrf_wcwidth(u);
+ }
+ }
+ if( mx>n ) n = mx;
+ if( pnNL ) *pnNL = nNL;
+ return n;
+}
+
+/*
+** Escape the input string if it is needed and in accordance with
+** eEsc, which is either QRF_ESC_Ascii or QRF_ESC_Symbol.
+**
+** Escaping is needed if the string contains any control characters
+** other than \t, \n, and \r\n
+**
+** If no escaping is needed (the common case) then set *ppOut to NULL
+** and return 0. If escaping is needed, write the escaped string into
+** memory obtained from sqlite3_malloc64() and make *ppOut point to that
+** memory and return 0. If an error occurs, return non-zero.
+**
+** The caller is responsible for freeing *ppFree if it is non-NULL in order
+** to reclaim memory.
+*/
+static void qrfEscape(
+ int eEsc, /* QRF_ESC_Ascii or QRF_ESC_Symbol */
+ sqlite3_str *pStr, /* String to be escaped */
+ int iStart /* Begin escapding on this byte of pStr */
+){
+ sqlite3_int64 i, j; /* Loop counters */
+ sqlite3_int64 sz; /* Size of the string prior to escaping */
+ sqlite3_int64 nCtrl = 0;/* Number of control characters to escape */
+ unsigned char *zIn; /* Text to be escaped */
+ unsigned char c; /* A single character of the text */
+ unsigned char *zOut; /* Where to write the results */
+
+ /* Find the text to be escaped */
+ zIn = (unsigned char*)sqlite3_str_value(pStr);
+ if( zIn==0 ) return;
+ zIn += iStart;
+
+ /* Count the control characters */
+ for(i=0; (c = zIn[i])!=0; i++){
+ if( c<=0x1f
+ && c!='\t'
+ && c!='\n'
+ && (c!='\r' || zIn[i+1]!='\n')
+ ){
+ nCtrl++;
+ }
+ }
+ if( nCtrl==0 ) return; /* Early out if no control characters */
+
+ /* Make space to hold the escapes. Copy the original text to the end
+ ** of the available space. */
+ sz = sqlite3_str_length(pStr) - iStart;
+ if( eEsc==QRF_ESC_Symbol ) nCtrl *= 2;
+ sqlite3_str_appendchar(pStr, nCtrl, ' ');
+ zOut = (unsigned char*)sqlite3_str_value(pStr);
+ if( zOut==0 ) return;
+ zOut += iStart;
+ zIn = zOut + nCtrl;
+ memmove(zIn,zOut,sz);
+
+ /* Convert the control characters */
+ for(i=j=0; (c = zIn[i])!=0; i++){
+ if( c>0x1f
+ || c=='\t'
+ || c=='\n'
+ || (c=='\r' && zIn[i+1]=='\n')
+ ){
+ continue;
+ }
+ if( i>0 ){
+ memmove(&zOut[j], zIn, i);
+ j += i;
+ }
+ zIn += i+1;
+ i = -1;
+ if( eEsc==QRF_ESC_Symbol ){
+ zOut[j++] = 0xe2;
+ zOut[j++] = 0x90;
+ zOut[j++] = 0x80+c;
+ }else{
+ zOut[j++] = '^';
+ zOut[j++] = 0x40+c;
+ }
+ }
+}
+
+/*
+** If a field contains any character identified by a 1 in the following
+** array, then the string must be quoted for CSV.
+*/
+static const char qrfCsvQuote[] = {
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+};
+
+/*
+** Encode text appropriately and append it to pOut.
+*/
+static void qrfEncodeText(Qrf *p, sqlite3_str *pOut, const char *zTxt){
+ int iStart = sqlite3_str_length(pOut);
+ switch( p->spec.eText ){
+ case QRF_TEXT_Sql: {
+ if( p->spec.eEsc==QRF_ESC_Off ){
+ sqlite3_str_appendf(pOut, "%Q", zTxt);
+ }else{
+ sqlite3_str_appendf(pOut, "%#Q", zTxt);
+ }
+ break;
+ }
+ case QRF_TEXT_Csv: {
+ unsigned int i;
+ for(i=0; zTxt[i]; i++){
+ if( qrfCsvQuote[((const unsigned char*)zTxt)[i]] ){
+ i = 0;
+ break;
+ }
+ }
+ if( i==0 || strstr(zTxt, p->spec.zColumnSep)!=0 ){
+ sqlite3_str_appendf(pOut, "\"%w\"", zTxt);
+ }else{
+ sqlite3_str_appendall(pOut, zTxt);
+ }
+ break;
+ }
+ case QRF_TEXT_Html: {
+ const unsigned char *z = (const unsigned char*)zTxt;
+ while( *z ){
+ unsigned int i = 0;
+ unsigned char c;
+ while( (c=z[i])>'>'
+ || (c && c!='<' && c!='>' && c!='&' && c!='\"' && c!='\'')
+ ){
+ i++;
+ }
+ if( i>0 ){
+ sqlite3_str_append(pOut, (const char*)z, i);
+ }
+ switch( z[i] ){
+ case '>': sqlite3_str_append(pOut, "<", 4); break;
+ case '&': sqlite3_str_append(pOut, "&", 5); break;
+ case '<': sqlite3_str_append(pOut, "<", 4); break;
+ case '"': sqlite3_str_append(pOut, """, 6); break;
+ case '\'': sqlite3_str_append(pOut, "'", 5); break;
+ default: i--;
+ }
+ z += i + 1;
+ }
+ break;
+ }
+ case QRF_TEXT_Tcl:
+ case QRF_TEXT_Json: {
+ const unsigned char *z = (const unsigned char*)zTxt;
+ sqlite3_str_append(pOut, "\"", 1);
+ while( *z ){
+ unsigned int i;
+ for(i=0; z[i]>=0x20 && z[i]!='\\' && z[i]!='"'; i++){}
+ if( i>0 ){
+ sqlite3_str_append(pOut, (const char*)z, i);
+ }
+ if( z[i]==0 ) break;
+ switch( z[i] ){
+ case '"': sqlite3_str_append(pOut, "\\\"", 2); break;
+ case '\\': sqlite3_str_append(pOut, "\\\\", 2); break;
+ case '\b': sqlite3_str_append(pOut, "\\b", 2); break;
+ case '\f': sqlite3_str_append(pOut, "\\f", 2); break;
+ case '\n': sqlite3_str_append(pOut, "\\n", 2); break;
+ case '\r': sqlite3_str_append(pOut, "\\r", 2); break;
+ case '\t': sqlite3_str_append(pOut, "\\t", 2); break;
+ default: {
+ if( p->spec.eText==QRF_TEXT_Json ){
+ sqlite3_str_appendf(pOut, "\\u%04x", z[i]);
+ }else{
+ sqlite3_str_appendf(pOut, "\\%03o", z[i]);
+ }
+ break;
+ }
+ }
+ z += i + 1;
+ }
+ sqlite3_str_append(pOut, "\"", 1);
+ break;
+ }
+ default: {
+ sqlite3_str_appendall(pOut, zTxt);
+ break;
+ }
+ }
+ if( p->spec.eEsc!=QRF_ESC_Off ){
+ qrfEscape(p->spec.eEsc, pOut, iStart);
+ }
+}
+
+/*
+** The current iCol-th column of p->pStmt is known to be a BLOB. Check
+** to see if that BLOB is really a JSONB blob. If it is, then translate
+** it into a text JSON representation and return a pointer to that text JSON.
+**
+** The memory used to hold the JSON text is managed internally by the
+** "p" object and is overwritten and/or deallocated upon the next call
+** to this routine (with the same p argument) or when the p object is
+** finailized.
+*/
+static const char *qrfJsonbToJson(Qrf *p, int iCol){
+ int nByte;
+ const void *pBlob;
+ int rc;
+ if( p->pJTrans==0 ){
+ sqlite3 *db;
+ rc = sqlite3_open(":memory:",&db);
+ if( rc ){
+ sqlite3_close(db);
+ return 0;
+ }
+ rc = sqlite3_prepare_v2(db, "SELECT json(?1)", -1, &p->pJTrans, 0);
+ if( rc ){
+ sqlite3_finalize(p->pJTrans);
+ p->pJTrans = 0;
+ sqlite3_close(db);
+ return 0;
+ }
+ }else{
+ sqlite3_reset(p->pJTrans);
+ }
+ nByte = sqlite3_column_bytes(p->pStmt, iCol);
+ pBlob = sqlite3_column_blob(p->pStmt, iCol);
+ sqlite3_bind_blob(p->pJTrans, 1, (void*)pBlob, nByte, SQLITE_STATIC);
+ rc = sqlite3_step(p->pJTrans);
+ if( rc==SQLITE_ROW ){
+ return (const char*)sqlite3_column_text(p->pJTrans, 0);
+ }else{
+ return 0;
+ }
+}
+
+/*
+** Render value pVal into pOut
+*/
+static void qrfRenderValue(Qrf *p, sqlite3_str *pOut, int iCol){
+ if( p->spec.xRender ){
+ sqlite3_value *pVal;
+ char *z;
+ pVal = sqlite3_value_dup(sqlite3_column_value(p->pStmt,iCol));
+ z = p->spec.xRender(p->spec.pRenderArg, pVal);
+ sqlite3_value_free(pVal);
+ if( z ){
+ sqlite3_str_appendall(pOut, z);
+ sqlite3_free(z);
+ return;
+ }
+ }
+ switch( sqlite3_column_type(p->pStmt,iCol) ){
+ case SQLITE_INTEGER: {
+ sqlite3_str_appendf(pOut, "%lld", sqlite3_column_int64(p->pStmt,iCol));
+ break;
+ }
+ case SQLITE_FLOAT: {
+ const char *zTxt = (const char*)sqlite3_column_text(p->pStmt,iCol);
+ sqlite3_str_appendall(pOut, zTxt);
+ break;
+ }
+ case SQLITE_BLOB: {
+ if( p->spec.bTextJsonb==QRF_Yes ){
+ const char *zJson = qrfJsonbToJson(p, iCol);
+ if( zJson ){
+ qrfEncodeText(p, pOut, zJson);
+ break;
+ }
+ }
+ switch( p->spec.eBlob ){
+ case QRF_BLOB_Hex:
+ case QRF_BLOB_Sql: {
+ int iStart;
+ int nBlob = sqlite3_column_bytes(p->pStmt,iCol);
+ int i, j;
+ char *zVal;
+ const unsigned char *a = sqlite3_column_blob(p->pStmt,iCol);
+ if( p->spec.eBlob==QRF_BLOB_Sql ){
+ sqlite3_str_append(pOut, "x'", 2);
+ }
+ iStart = sqlite3_str_length(pOut);
+ sqlite3_str_appendchar(pOut, nBlob, ' ');
+ sqlite3_str_appendchar(pOut, nBlob, ' ');
+ if( p->spec.eBlob==QRF_BLOB_Sql ){
+ sqlite3_str_appendchar(pOut, 1, '\'');
+ }
+ if( sqlite3_str_errcode(pOut) ) return;
+ zVal = sqlite3_str_value(pOut);
+ for(i=0, j=iStart; i<nBlob; i++, j+=2){
+ unsigned char c = a[i];
+ zVal[j] = "0123456789abcdef"[(c>>4)&0xf];
+ zVal[j+1] = "0123456789abcdef"[(c)&0xf];
+ }
+ break;
+ }
+ case QRF_BLOB_Tcl:
+ case QRF_BLOB_Json: {
+ int iStart;
+ int nBlob = sqlite3_column_bytes(p->pStmt,iCol);
+ int i, j;
+ char *zVal;
+ const unsigned char *a = sqlite3_column_blob(p->pStmt,iCol);
+ int szC = p->spec.eBlob==QRF_BLOB_Json ? 6 : 4;
+ sqlite3_str_append(pOut, "\"", 1);
+ iStart = sqlite3_str_length(pOut);
+ for(i=szC; i>0; i--){
+ sqlite3_str_appendchar(pOut, nBlob, ' ');
+ }
+ sqlite3_str_appendchar(pOut, 1, '"');
+ if( sqlite3_str_errcode(pOut) ) return;
+ zVal = sqlite3_str_value(pOut);
+ for(i=0, j=iStart; i<nBlob; i++, j+=szC){
+ unsigned char c = a[i];
+ zVal[j] = '\\';
+ if( szC==4 ){
+ zVal[j+1] = '0' + ((c>>6)&3);
+ zVal[j+2] = '0' + ((c>>3)&7);
+ zVal[j+3] = '0' + (c&7);
+ }else{
+ zVal[j+1] = 'u';
+ zVal[j+2] = '0';
+ zVal[j+3] = '0';
+ zVal[j+4] = "0123456789abcdef"[(c>>4)&0xf];
+ zVal[j+5] = "0123456789abcdef"[(c)&0xf];
+ }
+ }
+ break;
+ }
+ default: {
+ const char *zTxt = (const char*)sqlite3_column_text(p->pStmt,iCol);
+ qrfEncodeText(p, pOut, zTxt);
+ }
+ }
+ break;
+ }
+ case SQLITE_NULL: {
+ if( p->spec.bTextNull==QRF_Yes ){
+ qrfEncodeText(p, pOut, p->spec.zNull);
+ }else{
+ sqlite3_str_appendall(pOut, p->spec.zNull);
+ }
+ break;
+ }
+ case SQLITE_TEXT: {
+ const char *zTxt = (const char*)sqlite3_column_text(p->pStmt,iCol);
+ qrfEncodeText(p, pOut, zTxt);
+ break;
+ }
+ }
+}
+
+/*
+** Store string zUtf to pOut as w characters. If w is negative,
+** then right-justify the text. W is the width in display characters, not
+** in bytes. Double-width unicode characters count as two characters.
+** VT100 escape sequences count as zero. And so forth.
+*/
+static void qrfWidthPrint(Qrf *p, sqlite3_str *pOut, int w, const char *zUtf){
+ const unsigned char *a = (const unsigned char*)zUtf;
+ static const int mxW = 10000000;
+ unsigned char c;
+ int i = 0;
+ int n = 0;
+ int k;
+ int aw;
+ if( w<-mxW ){
+ w = -mxW;
+ }else if( w>mxW ){
+ w= mxW;
+ }
+ aw = w<0 ? -w : w;
+ if( a==0 ) a = (const unsigned char*)"";
+ while( (c = a[i])!=0 ){
+ if( (c&0xc0)==0xc0 ){
+ int u;
+ int len = sqlite3_qrf_decode_utf8(a+i, &u);
+ int x = sqlite3_qrf_wcwidth(u);
+ if( x+n>aw ){
+ break;
+ }
+ i += len;
+ n += x;
+ }else if( c==0x1b && (k = qrfIsVt100(&a[i]))>0 ){
+ i += k;
+ }else if( n>=aw ){
+ break;
+ }else{
+ n++;
+ i++;
+ }
+ }
+ if( n>=aw ){
+ sqlite3_str_append(pOut, zUtf, i);
+ }else if( w<0 ){
+ if( aw>n ) sqlite3_str_appendchar(pOut, aw-n, ' ');
+ sqlite3_str_append(pOut, zUtf, i);
+ }else{
+ sqlite3_str_append(pOut, zUtf, i);
+ if( aw>n ) sqlite3_str_appendchar(pOut, aw-n, ' ');
+ }
+}
+
+/*
+** Data for columnar layout, collected into a single object so
+** that it can be more easily passed into subroutines.
+*/
+typedef struct qrfColData qrfColData;
+struct qrfColData {
+ Qrf *p; /* The QRF instance */
+ int nCol; /* Number of columns in the table */
+ unsigned char bMultiRow; /* One or more cells will span multiple lines */
+ sqlite3_int64 nRow; /* Number of rows */
+ sqlite3_int64 nAlloc; /* Number of cells allocated */
+ sqlite3_int64 n; /* Number of cells. nCol*nRow */
+ char **azThis; /* Cache of pointers to current row */
+ char **az; /* Content of all cells */
+ int *aiWth; /* Width of each cell */
+ int *aiCol; /* Width of each column */
+ unsigned char *aAlign; /* Alignment for each column */
+};
+
+/*
+** Free all the memory allocates in the qrfColData object
+*/
+static void qrfColDataFree(qrfColData *p){
+ sqlite3_int64 i;
+ for(i=0; i<p->n; i++) sqlite3_free(p->az[i]);
+ sqlite3_free(p->az);
+ sqlite3_free(p->aiWth);
+ sqlite3_free(p->azThis);
+ memset(p, 0, sizeof(*p));
+}
+
+/*
+** Allocate space for more cells in the qrfColData object.
+** Return non-zero if a memory allocation fails.
+*/
+static int qrfColDataEnlarge(qrfColData *p){
+ char **azData;
+ int *aiWth;
+ p->nAlloc = 2*p->nAlloc + 10*p->nCol;
+ azData = sqlite3_realloc64(p->az, p->nAlloc*sizeof(char*));
+ if( azData==0 ){
+ qrfOom(p->p);
+ qrfColDataFree(p);
+ return 1;
+ }
+ p->az = azData;
+ aiWth = sqlite3_realloc64(p->aiWth, p->nAlloc*sizeof(int));
+ if( aiWth==0 ){
+ qrfOom(p->p);
+ qrfColDataFree(p);
+ return 1;
+ }
+ p->aiWth = aiWth;
+ return 0;
+}
+
+/*
+** (*pz)[] is a line of text that is to be displayed the box or table or
+** similar tabular formats. z[] contain newlines or might be too wide
+** to fit in the columns so will need to be split into multiple line.
+**
+** This routine determines:
+**
+** * How many bytes of z[] should be shown on the current line.
+** * How many character positions those bytes will cover.
+** * The byte offset to the start of the next line.
+*/
+static void qrfWrapLine(
+ const char *zIn, /* Input text to be displayed */
+ int w, /* Column width in characters (not bytes) */
+ int bWrap, /* True if we should do word-wrapping */
+ int *pnThis, /* OUT: How many bytes of z[] for the current line */
+ int *pnWide, /* OUT: How wide is the text of this line */
+ int *piNext /* OUT: Offset into z[] to start of the next line */
+){
+ int i; /* Input bytes consumed */
+ int k; /* Bytes in a VT100 code */
+ int n; /* Output column number */
+ const unsigned char *z = (const unsigned char*)zIn;
+ unsigned char c = 0;
+
+ if( zIn[0]==0 ){
+ *pnThis = 0;
+ *pnWide = 0;
+ *piNext = 0;
+ return;
+ }
+ n = 0;
+ for(i=0; n<w; i++){
+ c = zIn[i];
+ if( c>=0xc0 ){
+ int u;
+ int len = sqlite3_qrf_decode_utf8(&z[i], &u);
+ int wcw = sqlite3_qrf_wcwidth(u);
+ if( wcw+n>w ) break;
+ i += len-1;
+ n += wcw;
+ continue;
+ }
+ if( c>=' ' ){
+ n++;
+ continue;
+ }
+ if( c==0 || c=='\n' ) break;
+ if( c=='\r' && zIn[i+1]=='\n' ){ c = zIn[++i]; break; }
+ if( c=='\t' ){
+ int wcw = 8 - (n&7);
+ if( n+wcw>w ) break;
+ n += wcw;
+ continue;
+ }
+ if( c==0x1b && (k = qrfIsVt100(&z[i]))>0 ){
+ i += k-1;
+ }
+ }
+ if( c==0 ){
+ *pnThis = i;
+ *pnWide = n;
+ *piNext = i;
+ return;
+ }
+ if( c=='\n' ){
+ *pnThis = i;
+ *pnWide = n;
+ *piNext = i+1;
+ return;
+ }
+
+ /* If we get this far, that means the current line will end at some
+ ** point that is neither a "\n" or a 0x00. Figure out where that
+ ** split should occur
+ */
- if( isspace(z[k]) ) break;
++ if( bWrap && z[i]!=0 && !qrfSpace(z[i]) && qrfAlnum(c)==qrfAlnum(z[i]) ){
+ /* Perhaps try to back up to a better place to break the line */
+ for(k=i-1; k>=i/2; k--){
- if( isalnum(z[k-1])!=isalnum(z[k]) && (z[k]&0xc0)!=0x80 ) break;
++ if( qrfSpace(z[k]) ) break;
+ }
+ if( k<i/2 ){
+ for(k=i; k>=i/2; k--){
- if( !isalpha(z[0]) && z[0]!='_' ) return 1;
++ if( qrfAlnum(z[k-1])!=qrfAlnum(z[k]) && (z[k]&0xc0)!=0x80 ) break;
+ }
+ }
+ if( k>=i/2 ){
+ i = k;
+ n = qrfDisplayWidth((const char*)z, k, 0);
+ }
+ }
+ *pnThis = i;
+ *pnWide = n;
+ while( zIn[i]==' ' || zIn[i]=='\t' || zIn[i]=='\r' ){ i++; }
+ *piNext = i;
+}
+
+/*
+** Print a markdown or table-style row separator using ascii-art
+*/
+static void qrfRowSeparator(sqlite3_str *pOut, qrfColData *p, char cSep){
+ int i;
+ if( p->nCol>0 ){
+ sqlite3_str_append(pOut, &cSep, 1);
+ sqlite3_str_appendchar(pOut, p->aiCol[0]+2, '-');
+ for(i=1; i<p->nCol; i++){
+ sqlite3_str_append(pOut, &cSep, 1);
+ sqlite3_str_appendchar(pOut, p->aiCol[i]+2, '-');
+ }
+ sqlite3_str_append(pOut, &cSep, 1);
+ }
+ sqlite3_str_append(pOut, "\n", 1);
+}
+
+/*
+** UTF8 box-drawing characters. Imagine box lines like this:
+**
+** 1
+** |
+** 4 --+-- 2
+** |
+** 3
+**
+** Each box characters has between 2 and 4 of the lines leading from
+** the center. The characters are here identified by the numbers of
+** their corresponding lines.
+*/
+#define BOX_24 "\342\224\200" /* U+2500 --- */
+#define BOX_13 "\342\224\202" /* U+2502 | */
+#define BOX_23 "\342\224\214" /* U+250c ,- */
+#define BOX_34 "\342\224\220" /* U+2510 -, */
+#define BOX_12 "\342\224\224" /* U+2514 '- */
+#define BOX_14 "\342\224\230" /* U+2518 -' */
+#define BOX_123 "\342\224\234" /* U+251c |- */
+#define BOX_134 "\342\224\244" /* U+2524 -| */
+#define BOX_234 "\342\224\254" /* U+252c -,- */
+#define BOX_124 "\342\224\264" /* U+2534 -'- */
+#define BOX_1234 "\342\224\274" /* U+253c -|- */
+
+/* Draw horizontal line N characters long using unicode box
+** characters
+*/
+static void qrfBoxLine(sqlite3_str *pOut, int N){
+ const char zDash[] =
+ BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24
+ BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24 BOX_24;
+ const int nDash = sizeof(zDash) - 1;
+ N *= 3;
+ while( N>nDash ){
+ sqlite3_str_append(pOut, zDash, nDash);
+ N -= nDash;
+ }
+ sqlite3_str_append(pOut, zDash, N);
+}
+
+/*
+** Draw a horizontal separator for a QRF_STYLE_Box table.
+*/
+static void qrfBoxSeparator(
+ sqlite3_str *pOut,
+ qrfColData *p,
+ const char *zSep1,
+ const char *zSep2,
+ const char *zSep3
+){
+ int i;
+ if( p->nCol>0 ){
+ sqlite3_str_appendall(pOut, zSep1);
+ qrfBoxLine(pOut, p->aiCol[0]+2);
+ for(i=1; i<p->nCol; i++){
+ sqlite3_str_appendall(pOut, zSep2);
+ qrfBoxLine(pOut, p->aiCol[i]+2);
+ }
+ sqlite3_str_appendall(pOut, zSep3);
+ }
+ sqlite3_str_append(pOut, "\n", 1);
+}
+
+/*
+** Load into pData the default alignment for the body of a table.
+*/
+static void qrfLoadAlignment(qrfColData *pData, Qrf *p){
+ sqlite3_int64 i;
+ memset(pData->aAlign, p->spec.eDfltAlign, pData->nCol);
+ for(i=0; i<pData->nCol; i++){
+ if( i<p->spec.nAlign ){
+ unsigned char ax = p->spec.aAlign[i];
+ if( (ax & QRF_ALIGN_HMASK)!=0 ){
+ pData->aAlign[i] = (ax & QRF_ALIGN_HMASK) |
+ (pData->aAlign[i] & QRF_ALIGN_VMASK);
+ }
+ }else if( i<p->spec.nWidth ){
+ if( p->spec.aWidth[i]<0 ){
+ pData->aAlign[i] = QRF_ALIGN_Right |
+ (pData->aAlign[i] & QRF_ALIGN_VMASK);
+ }
+ }else{
+ break;
+ }
+ }
+}
+
+/*
+** Output horizontally justified text into pOut. The text is the
+** first nVal bytes of zVal. Include nWS bytes of whitespace, either
+** split between both sides, or on the left, or on the right, depending
+** on eAlign.
+*/
+static void qrfPrintAligned(
+ sqlite3_str *pOut, /* Append text here */
+ const char *zVal, /* Text to append */
+ int nVal, /* Use only the first nVal bytes of zVal[] */
+ int nWS, /* Whitespace for horizonal alignment */
+ unsigned char eAlign /* Alignment type */
+){
+ eAlign &= QRF_ALIGN_HMASK;
+ if( eAlign==QRF_ALIGN_Center ){
+ /* Center the text */
+ sqlite3_str_appendchar(pOut, nWS/2, ' ');
+ sqlite3_str_append(pOut, zVal, nVal);
+ sqlite3_str_appendchar(pOut, nWS - nWS/2, ' ');
+ }else if( eAlign==QRF_ALIGN_Right){
+ /* Right justify the text */
+ sqlite3_str_appendchar(pOut, nWS, ' ');
+ sqlite3_str_append(pOut, zVal, nVal);
+ }else{
+ /* Left justify the next */
+ sqlite3_str_append(pOut, zVal, nVal);
+ sqlite3_str_appendchar(pOut, nWS, ' ');
+ }
+}
+
+/*
+** Columnar modes require that the entire query be evaluated first, with
+** results written into memory, so that we can compute appropriate column
+** widths.
+*/
+static void qrfColumnar(Qrf *p){
+ sqlite3_int64 i, j; /* Loop counters */
+ const char *colSep = 0; /* Column separator text */
+ const char *rowSep = 0; /* Row terminator text */
+ const char *rowStart = 0; /* Row start text */
+ int szColSep, szRowSep, szRowStart; /* Size in bytes of previous 3 */
+ int rc; /* Result code */
+ int nColumn = p->nCol; /* Number of columns */
+ int bWW; /* True to do word-wrap */
+ sqlite3_str *pStr; /* Temporary rendering */
+ qrfColData data; /* Columnar layout data */
+
+ rc = sqlite3_step(p->pStmt);
+ if( rc!=SQLITE_ROW || nColumn==0 ){
+ return; /* No output */
+ }
+
+ /* Initialize the data container */
+ memset(&data, 0, sizeof(data));
+ data.nCol = p->nCol;
+ data.azThis = sqlite3_malloc64( nColumn*(sizeof(char*) + sizeof(int) + 1) );
+ if( data.azThis==0 ){
+ qrfOom(p);
+ return;
+ }
+ data.aiCol = (int*)&data.azThis[nColumn];
+ data.aAlign = (unsigned char*)&data.aiCol[nColumn];
+ if( qrfColDataEnlarge(&data) ) return;
+ assert( data.az!=0 );
+ assert( data.aAlign!=0 );
+
+ /* Load the column header names and all cell content into data */
+ if( p->spec.bTitles==QRF_Yes ){
+ unsigned char saved_eText = p->spec.eText;
+ p->spec.eText = p->spec.eTitle;
+ for(i=0; i<nColumn; i++){
+ const char *z = (const char*)sqlite3_column_name(p->pStmt,i);
+ int nNL = 0;
+ int n;
+ pStr = sqlite3_str_new(p->db);
+ qrfEncodeText(p, pStr, z ? z : "");
+ n = sqlite3_str_length(pStr);
+ z = data.az[data.n] = sqlite3_str_finish(pStr);
+ data.aiWth[data.n] = qrfDisplayWidth(z, n, &nNL);
+ data.n++;
+ if( nNL ) data.bMultiRow = 1;
+ }
+ p->spec.eText = saved_eText;
+ p->nRow++;
+ }
+ do{
+ if( data.n+nColumn > data.nAlloc ){
+ if( qrfColDataEnlarge(&data) ) return;
+ }
+ for(i=0; i<nColumn; i++){
+ char *z;
+ int nNL = 0;
+ int n;
+ pStr = sqlite3_str_new(p->db);
+ qrfRenderValue(p, pStr, i);
+ n = sqlite3_str_length(pStr);
+ z = data.az[data.n] = sqlite3_str_finish(pStr);
+ data.aiWth[data.n] = qrfDisplayWidth(z, n, &nNL);
+ data.n++;
+ if( nNL ) data.bMultiRow = 1;
+ }
+ p->nRow++;
+ }while( sqlite3_step(p->pStmt)==SQLITE_ROW && p->iErr==SQLITE_OK );
+ if( p->iErr ){
+ qrfColDataFree(&data);
+ return;
+ }
+
+ /* Compute the width and alignment of every column */
+ if( p->spec.bTitles==QRF_No ){
+ qrfLoadAlignment(&data, p);
+ }else if( p->spec.eTitleAlign==QRF_Auto ){
+ memset(data.aAlign, QRF_ALIGN_Center, nColumn);
+ }else{
+ memset(data.aAlign, p->spec.eTitleAlign, nColumn);
+ }
+
+ for(i=0; i<nColumn; i++){
+ int w = 0;
+ if( i<p->spec.nWidth ){
+ w = p->spec.aWidth[i];
+ if( w==(-32768) ){
+ w = 0;
+ if( p->spec.nAlign>i && (p->spec.aAlign[i] & QRF_ALIGN_HMASK)==0 ){
+ data.aAlign[i] |= QRF_ALIGN_Right;
+ }
+ }else if( w<0 ){
+ w = -w;
+ if( p->spec.nAlign>i && (p->spec.aAlign[i] & QRF_ALIGN_HMASK)==0 ){
+ data.aAlign[i] |= QRF_ALIGN_Right;
+ }
+ }
+ }
+ if( w==0 ){
+ for(j=i; j<data.n; j+=nColumn){
+ if( data.aiWth[j] > w ){
+ w = data.aiWth[j];
+ if( p->spec.nWrap>0 && w>p->spec.nWrap ){
+ w = p->spec.nWrap;
+ data.bMultiRow = 1;
+ break;
+ }
+ }
+ }
+ }else if( data.bMultiRow==0 || w==1 ){
+ for(j=i; j<data.n; j+=nColumn){
+ if( data.aiWth[j] > w ){
+ data.bMultiRow = 1;
+ if( w==1 ){
+ /* If aiWth[j] is 2 or more, then there might be a double-wide
+ ** character somewhere. So make the column width at least 2. */
+ w = 2;
+ }
+ break;
+ }
+ }
+ }
+ data.aiCol[i] = w;
+ }
+
+ /* TBD: Narrow columns so that the total is less than p->spec.nScreenWidth */
+
+ /* Draw the line across the top of the table. Also initialize
+ ** the row boundary and column separator texts. */
+ switch( p->spec.eStyle ){
+ case QRF_STYLE_Box:
+ rowStart = BOX_13 " ";
+ colSep = " " BOX_13 " ";
+ rowSep = " " BOX_13 "\n";
+ qrfBoxSeparator(p->pOut, &data, BOX_23, BOX_234, BOX_34);
+ break;
+ case QRF_STYLE_Table:
+ rowStart = "| ";
+ colSep = " | ";
+ rowSep = " |\n";
+ qrfRowSeparator(p->pOut, &data, '+');
+ break;
+ case QRF_STYLE_Column:
+ rowStart = "";
+ colSep = " ";
+ rowSep = "\n";
+ break;
+ default: /*case QRF_STYLE_Markdown:*/
+ rowStart = "| ";
+ colSep = " | ";
+ rowSep = " |\n";
+ break;
+ }
+ szRowStart = (int)strlen(rowStart);
+ szRowSep = (int)strlen(rowSep);
+ szColSep = (int)strlen(colSep);
+
+ bWW = (p->spec.bWordWrap==QRF_Yes && data.bMultiRow);
+ for(i=0; i<data.n; i+=nColumn){
+ int bMore;
+ int nRow = 0;
+
+ /* Draw a single row of the table. This might be the title line
+ ** (if there is a title line) or a row in the body of the table.
+ ** The column number will be j. The row number is i/nColumn.
+ */
+ for(j=0; j<nColumn; j++){ data.azThis[j] = data.az[i+j]; }
+ do{
+ sqlite3_str_append(p->pOut, rowStart, szRowStart);
+ bMore = 0;
+ for(j=0; j<nColumn; j++){
+ int nThis = 0;
+ int nWide = 0;
+ int iNext = 0;
+ int nWS;
+ qrfWrapLine(data.azThis[j], data.aiCol[j], bWW, &nThis, &nWide, &iNext);
+ nWS = data.aiCol[j] - nWide;
+ qrfPrintAligned(p->pOut, data.azThis[j], nThis, nWS, data.aAlign[j]);
+ data.azThis[j] += iNext;
+ if( data.azThis[j][0]!=0 ) bMore = 1;
+ if( j<nColumn-1 ){
+ sqlite3_str_append(p->pOut, colSep, szColSep);
+ }else{
+ sqlite3_str_append(p->pOut, rowSep, szRowSep);
+ }
+ }
+ }while( bMore && ++nRow < p->mxHeight );
+ if( bMore ){
+ /* This row was terminated by nLineLimit. Show ellipsis. */
+ sqlite3_str_append(p->pOut, rowStart, szRowStart);
+ for(j=0; j<nColumn; j++){
+ if( data.azThis[j][0]==0 ){
+ sqlite3_str_appendchar(p->pOut, data.aiCol[j], ' ');
+ }else{
+ int nE = 3;
+ if( nE>data.aiCol[j] ) nE = data.aiCol[j];
+ qrfPrintAligned(p->pOut, "...", nE, data.aiCol[j]-nE, data.aAlign[j]);
+ }
+ if( j<nColumn-1 ){
+ sqlite3_str_append(p->pOut, colSep, szColSep);
+ }else{
+ sqlite3_str_append(p->pOut, rowSep, szRowSep);
+ }
+ }
+ }
+
+ /* Draw either (1) the separator between the title line and the body
+ ** of the table, or (2) separators between individual rows of the table
+ ** body. isTitleDataSeparator will be true if we are doing (1).
+ */
+ if( (i==0 || data.bMultiRow) && i+nColumn<data.n ){
+ int isTitleDataSeparator = (i==0 && p->spec.bTitles==QRF_Yes);
+ if( isTitleDataSeparator ){
+ qrfLoadAlignment(&data, p);
+ }
+ switch( p->spec.eStyle ){
+ case QRF_STYLE_Table: {
+ if( isTitleDataSeparator || data.bMultiRow ){
+ qrfRowSeparator(p->pOut, &data, '+');
+ }
+ break;
+ }
+ case QRF_STYLE_Box: {
+ if( isTitleDataSeparator || data.bMultiRow ){
+ qrfBoxSeparator(p->pOut, &data, BOX_123, BOX_1234, BOX_134);
+ }
+ break;
+ }
+ case QRF_STYLE_Markdown: {
+ if( isTitleDataSeparator ){
+ qrfRowSeparator(p->pOut, &data, '|');
+ }
+ break;
+ }
+ case QRF_STYLE_Column: {
+ if( isTitleDataSeparator ){
+ for(j=0; j<nColumn; j++){
+ sqlite3_str_appendchar(p->pOut, data.aiCol[j], '-');
+ if( j<nColumn-1 ){
+ sqlite3_str_append(p->pOut, colSep, szColSep);
+ }else{
+ sqlite3_str_append(p->pOut, rowSep, szRowSep);
+ }
+ }
+ }else if( data.bMultiRow ){
+ sqlite3_str_append(p->pOut, "\n", 1);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /* Draw the line across the bottom of the table */
+ switch( p->spec.eStyle ){
+ case QRF_STYLE_Box:
+ qrfBoxSeparator(p->pOut, &data, BOX_12, BOX_124, BOX_14);
+ break;
+ case QRF_STYLE_Table:
+ qrfRowSeparator(p->pOut, &data, '+');
+ break;
+ }
+ qrfWrite(p);
+
+ qrfColDataFree(&data);
+ 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 = 0; /* 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 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.eStyle==QRF_STYLE_StatsVm ){
+ 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 = (const char*)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);
+}
+
+/*
+** Do a "scanstatus vm" style EXPLAIN listing on p->pStmt.
+**
+** p->pStmt is probably not an EXPLAIN query. Instead, construct a
+** new query that is a bytecode() rendering of p->pStmt with extra
+** columns for the "scanstatus vm" outputs, and run the results of
+** that new query through the normal EXPLAIN formatting.
+*/
+static void qrfScanStatusVm(Qrf *p){
+ sqlite3_stmt *pOrigStmt = p->pStmt;
+ sqlite3_stmt *pExplain;
+ int rc;
+ static const char *zSql =
+ " SELECT addr, opcode, p1, p2, p3, p4, p5, comment, nexec,"
+ " format('% 6s (%.2f%%)',"
+ " CASE WHEN ncycle<100_000 THEN ncycle || ' '"
+ " WHEN ncycle<100_000_000 THEN (ncycle/1_000) || 'K'"
+ " WHEN ncycle<100_000_000_000 THEN (ncycle/1_000_000) || 'M'"
+ " ELSE (ncycle/1000_000_000) || 'G' END,"
+ " ncycle*100.0/(sum(ncycle) OVER ())"
+ " ) AS cycles"
+ " FROM bytecode(?1)";
+ rc = sqlite3_prepare_v2(p->db, zSql, -1, &pExplain, 0);
+ if( rc ){
+ qrfError(p, rc, "%s", sqlite3_errmsg(p->db));
+ sqlite3_finalize(pExplain);
+ return;
+ }
+ sqlite3_bind_pointer(pExplain, 1, pOrigStmt, "stmt-pointer", 0);
+ p->pStmt = pExplain;
+ p->nCol = 10;
+ qrfExplain(p);
+ sqlite3_finalize(pExplain);
+ p->pStmt = pOrigStmt;
+}
+
+/*
+** Attempt to determine if identifier zName needs to be quoted, either
+** because it contains non-alphanumeric characters, or because it is an
+** SQLite keyword. Be conservative in this estimate: When in doubt assume
+** that quoting is required.
+**
+** Return 1 if quoting is required. Return 0 if no quoting is required.
+*/
+
+static int qrf_need_quote(const char *zName){
+ int i;
+ const unsigned char *z = (const unsigned char*)zName;
+ if( z==0 ) return 1;
- if( !isalnum(z[i]) && z[i]!='_' ) return 1;
++ if( !qrfAlpha(z[0]) ) return 1;
+ for(i=0; z[i]; i++){
++ if( !qrfAlnum(z[i]) ) return 1;
+ }
+ return sqlite3_keyword_check(zName, i)!=0;
+}
+
+/*
+** Helper function for QRF_STYLE_Json and QRF_STYLE_JObject.
+** The initial "{" for a JSON object that will contain row content
+** has been output. Now output all the content.
+*/
+static void qrfOneJsonRow(Qrf *p){
+ int i, nItem;
+ for(nItem=i=0; i<p->nCol; i++){
+ const char *zCName;
+ zCName = sqlite3_column_name(p->pStmt, i);
+ if( nItem>0 ) sqlite3_str_append(p->pOut, ",", 1);
+ nItem++;
+ qrfEncodeText(p, p->pOut, zCName);
+ sqlite3_str_append(p->pOut, ":", 1);
+ qrfRenderValue(p, p->pOut, i);
+ }
+ qrfWrite(p);
+}
+
+/*
+** Render a single row of output for non-columnar styles - any
+** style that lets us render row by row as the content is received
+** from the query.
+*/
+static void qrfOneSimpleRow(Qrf *p){
+ int i;
+ switch( p->spec.eStyle ){
+ case QRF_STYLE_Off:
+ case QRF_STYLE_Count: {
+ /* No-op */
+ break;
+ }
+ case QRF_STYLE_Json: {
+ if( p->nRow==0 ){
+ sqlite3_str_append(p->pOut, "[{", 2);
+ }else{
+ sqlite3_str_append(p->pOut, "},\n{", 4);
+ }
+ qrfOneJsonRow(p);
+ break;
+ }
+ case QRF_STYLE_JObject: {
+ if( p->nRow==0 ){
+ sqlite3_str_append(p->pOut, "{", 1);
+ }else{
+ sqlite3_str_append(p->pOut, "}\n{", 3);
+ }
+ qrfOneJsonRow(p);
+ break;
+ }
+ case QRF_STYLE_Html: {
+ if( p->nRow==0 && p->spec.bTitles==QRF_Yes ){
+ sqlite3_str_append(p->pOut, "<TR>", 4);
+ for(i=0; i<p->nCol; i++){
+ const char *zCName = sqlite3_column_name(p->pStmt, i);
+ sqlite3_str_append(p->pOut, "\n<TH>", 5);
+ qrfEncodeText(p, p->pOut, zCName);
+ }
+ sqlite3_str_append(p->pOut, "\n</TR>\n", 7);
+ }
+ sqlite3_str_append(p->pOut, "<TR>", 4);
+ for(i=0; i<p->nCol; i++){
+ sqlite3_str_append(p->pOut, "\n<TD>", 5);
+ qrfRenderValue(p, p->pOut, i);
+ }
+ sqlite3_str_append(p->pOut, "\n</TR>\n", 7);
+ qrfWrite(p);
+ break;
+ }
+ case QRF_STYLE_Insert: {
+ if( qrf_need_quote(p->spec.zTableName) ){
+ sqlite3_str_appendf(p->pOut,"INSERT INTO \"%w\"",p->spec.zTableName);
+ }else{
+ sqlite3_str_appendf(p->pOut,"INSERT INTO %s",p->spec.zTableName);
+ }
+ if( p->spec.bTitles==QRF_Yes ){
+ for(i=0; i<p->nCol; i++){
+ const char *zCName = sqlite3_column_name(p->pStmt, i);
+ if( qrf_need_quote(zCName) ){
+ sqlite3_str_appendf(p->pOut, "%c\"%w\"",
+ i==0 ? '(' : ',', zCName);
+ }else{
+ sqlite3_str_appendf(p->pOut, "%c%s",
+ i==0 ? '(' : ',', zCName);
+ }
+ }
+ sqlite3_str_append(p->pOut, ")", 1);
+ }
+ sqlite3_str_append(p->pOut," VALUES(", 8);
+ for(i=0; i<p->nCol; i++){
+ if( i>0 ) sqlite3_str_append(p->pOut, ",", 1);
+ qrfRenderValue(p, p->pOut, i);
+ }
+ sqlite3_str_append(p->pOut, ");\n", 3);
+ qrfWrite(p);
+ break;
+ }
+ case QRF_STYLE_Line: {
+ sqlite3_str *pVal;
+ int mxW;
+ int bWW;
+ 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->u.sLine.mxColWth = 0;
+ for(i=0; i<p->nCol; i++){
+ int 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);
+ pVal = sqlite3_str_new(p->db);
+ mxW = p->mxWidth - (3 + p->u.sLine.mxColWth);
+ bWW = p->spec.bWordWrap==QRF_Yes;
+ for(i=0; i<p->nCol; i++){
+ const char *zVal;
+ int cnt = 0;
+ qrfWidthPrint(p, p->pOut, -p->u.sLine.mxColWth, p->u.sLine.azCol[i]);
+ sqlite3_str_append(p->pOut, " = ", 3);
+ qrfRenderValue(p, pVal, i);
+ zVal = sqlite3_str_value(pVal);
+ if( zVal==0 ) zVal = "";
+ do{
+ int nThis, nWide, iNext;
+ qrfWrapLine(zVal, mxW, bWW, &nThis, &nWide, &iNext);
+ if( cnt ) sqlite3_str_appendchar(p->pOut,p->u.sLine.mxColWth+3,' ');
+ cnt++;
+ if( cnt>p->mxHeight ){
+ zVal = "...";
+ nThis = iNext = 3;
+ }
+ sqlite3_str_append(p->pOut, zVal, nThis);
+ sqlite3_str_append(p->pOut, "\n", 1);
+ zVal += iNext;
+ }while( zVal[0] );
+ sqlite3_str_reset(pVal);
+ }
+ sqlite3_free(sqlite3_str_finish(pVal));
+ qrfWrite(p);
+ break;
+ }
+ case QRF_STYLE_Eqp: {
+ const char *zEqpLine = (const char*)sqlite3_column_text(p->pStmt,3);
+ int iEqpId = sqlite3_column_int(p->pStmt, 0);
+ int iParentId = sqlite3_column_int(p->pStmt, 1);
+ if( zEqpLine==0 ) zEqpLine = "";
+ if( zEqpLine[0]=='-' ) qrfEqpRender(p, 0);
+ qrfEqpAppend(p, iEqpId, iParentId, zEqpLine);
+ break;
+ }
+ default: { /* QRF_STYLE_List */
+ if( p->nRow==0 && p->spec.bTitles==QRF_Yes ){
+ int saved_eText = p->spec.eText;
+ p->spec.eText = p->spec.eTitle;
+ for(i=0; i<p->nCol; i++){
+ const char *zCName = sqlite3_column_name(p->pStmt, i);
+ if( i>0 ) sqlite3_str_appendall(p->pOut, p->spec.zColumnSep);
+ qrfEncodeText(p, p->pOut, zCName);
+ }
+ sqlite3_str_appendall(p->pOut, p->spec.zRowSep);
+ qrfWrite(p);
+ p->spec.eText = saved_eText;
+ }
+ for(i=0; i<p->nCol; i++){
+ if( i>0 ) sqlite3_str_appendall(p->pOut, p->spec.zColumnSep);
+ qrfRenderValue(p, p->pOut, i);
+ }
+ sqlite3_str_appendall(p->pOut, p->spec.zRowSep);
+ qrfWrite(p);
+ break;
+ }
+ }
+ p->nRow++;
+}
+
+/*
+** Initialize the internal Qrf object.
+*/
+static void qrfInitialize(
+ Qrf *p, /* State object to be initialized */
+ sqlite3_stmt *pStmt, /* Query whose output to be formatted */
+ const sqlite3_qrf_spec *pSpec, /* Format specification */
+ char **pzErr /* Write errors here */
+){
+ size_t sz; /* Size of pSpec[], based on pSpec->iVersion */
+ memset(p, 0, sizeof(*p));
+ p->pzErr = pzErr;
+ if( pSpec->iVersion!=1 ){
+ qrfError(p, SQLITE_ERROR,
+ "unusable sqlite3_qrf_spec.iVersion (%d)",
+ pSpec->iVersion);
+ return;
+ }
+ p->pStmt = pStmt;
+ p->db = sqlite3_db_handle(pStmt);
+ p->pOut = sqlite3_str_new(p->db);
+ if( p->pOut==0 ){
+ qrfOom(p);
+ return;
+ }
+ p->iErr = 0;
+ p->nCol = sqlite3_column_count(p->pStmt);
+ p->nRow = 0;
+ sz = sizeof(sqlite3_qrf_spec);
+ memcpy(&p->spec, pSpec, sz);
+ if( p->spec.zNull==0 ) p->spec.zNull = "";
+ p->mxWidth = p->spec.nScreenWidth;
+ if( p->mxWidth<=0 ) p->mxWidth = QRF_MAX_WIDTH;
+ p->mxHeight = p->spec.nLineLimit;
+ if( p->mxHeight<=0 ) p->mxHeight = 2147483647;
+qrf_reinit:
+ switch( p->spec.eStyle ){
+ case QRF_Auto: {
+ switch( sqlite3_stmt_isexplain(pStmt) ){
+ case 0: p->spec.eStyle = QRF_STYLE_Box; break;
+ case 1: p->spec.eStyle = QRF_STYLE_Explain; break;
+ default: p->spec.eStyle = QRF_STYLE_Eqp; break;
+ }
+ goto qrf_reinit;
+ }
+ case QRF_STYLE_List: {
+ if( p->spec.zColumnSep==0 ) p->spec.zColumnSep = "|";
+ if( p->spec.zRowSep==0 ) p->spec.zRowSep = "\n";
+ break;
+ }
+ case QRF_STYLE_JObject:
+ case QRF_STYLE_Json: {
+ p->spec.eText = QRF_TEXT_Json;
+ p->spec.eBlob = QRF_BLOB_Json;
+ p->spec.zNull = "null";
+ break;
+ }
+ case QRF_STYLE_Html: {
+ p->spec.eText = QRF_TEXT_Html;
+ p->spec.zNull = "null";
+ break;
+ }
+ case QRF_STYLE_Insert: {
+ p->spec.eText = QRF_TEXT_Sql;
+ p->spec.eBlob = QRF_BLOB_Sql;
+ p->spec.zNull = "NULL";
+ if( p->spec.zTableName==0 || p->spec.zTableName[0]==0 ){
+ p->spec.zTableName = "tab";
+ }
+ break;
+ }
+ case QRF_STYLE_Csv: {
+ p->spec.eStyle = QRF_STYLE_List;
+ p->spec.eText = QRF_TEXT_Csv;
+ p->spec.eBlob = QRF_BLOB_Tcl;
+ p->spec.zColumnSep = ",";
+ p->spec.zRowSep = "\r\n";
+ break;
+ }
+ case QRF_STYLE_Quote: {
+ p->spec.eText = QRF_TEXT_Sql;
+ p->spec.eBlob = QRF_BLOB_Sql;
+ p->spec.zNull = "NULL";
+ p->spec.zColumnSep = ",";
+ p->spec.zRowSep = "\n";
+ break;
+ }
+ case QRF_STYLE_Eqp: {
+ int expMode = sqlite3_stmt_isexplain(p->pStmt);
+ if( expMode!=2 ){
+ sqlite3_stmt_explain(p->pStmt, 2);
+ p->expMode = expMode+1;
+ }
+ break;
+ }
+ case QRF_STYLE_Explain: {
+ int expMode = sqlite3_stmt_isexplain(p->pStmt);
+ if( expMode!=1 ){
+ sqlite3_stmt_explain(p->pStmt, 1);
+ p->expMode = expMode+1;
+ }
+ break;
+ }
+ }
+ if( p->spec.eEsc==QRF_Auto ){
+ p->spec.eEsc = QRF_ESC_Ascii;
+ }
+ if( p->spec.eText==QRF_Auto ){
+ p->spec.eText = QRF_TEXT_Plain;
+ }
+ if( p->spec.eTitle==QRF_Auto ){
+ switch( p->spec.eStyle ){
+ case QRF_STYLE_Box:
+ case QRF_STYLE_Column:
+ case QRF_STYLE_Table:
+ p->spec.eTitle = QRF_TEXT_Plain;
+ break;
+ default:
+ p->spec.eTitle = p->spec.eText;
+ break;
+ }
+ }
+ if( p->spec.eBlob==QRF_Auto ){
+ switch( p->spec.eText ){
+ case QRF_TEXT_Sql: p->spec.eBlob = QRF_BLOB_Sql; break;
+ case QRF_TEXT_Csv: p->spec.eBlob = QRF_BLOB_Tcl; break;
+ case QRF_TEXT_Tcl: p->spec.eBlob = QRF_BLOB_Tcl; break;
+ case QRF_TEXT_Json: p->spec.eBlob = QRF_BLOB_Json; break;
+ default: p->spec.eBlob = QRF_BLOB_Text; break;
+ }
+ }
+ if( p->spec.bTitles==QRF_Auto ){
+ switch( p->spec.eStyle ){
+ case QRF_STYLE_Box:
+ case QRF_STYLE_Csv:
+ case QRF_STYLE_Column:
+ case QRF_STYLE_Table:
+ case QRF_STYLE_Markdown:
+ p->spec.bTitles = QRF_Yes;
+ break;
+ default:
+ p->spec.bTitles = QRF_No;
+ break;
+ }
+ }
+ if( p->spec.bWordWrap==QRF_Auto ){
+ p->spec.bWordWrap = QRF_Yes;
+ }
+ if( p->spec.bTextJsonb==QRF_Auto ){
+ p->spec.bTextJsonb = QRF_No;
+ }
+ if( p->spec.zColumnSep==0 ) p->spec.zColumnSep = ",";
+ if( p->spec.zRowSep==0 ) p->spec.zRowSep = "\n";
+}
+
+/*
+** Finish rendering the results
+*/
+static void qrfFinalize(Qrf *p){
+ switch( p->spec.eStyle ){
+ case QRF_STYLE_Count: {
+ sqlite3_str_appendf(p->pOut, "%lld\n", p->nRow);
+ qrfWrite(p);
+ break;
+ }
+ case QRF_STYLE_Json: {
+ sqlite3_str_append(p->pOut, "}]\n", 3);
+ qrfWrite(p);
+ break;
+ }
+ case QRF_STYLE_JObject: {
+ sqlite3_str_append(p->pOut, "}\n", 2);
+ qrfWrite(p);
+ break;
+ }
+ case QRF_STYLE_Line: {
+ if( p->u.sLine.azCol ) sqlite3_free(p->u.sLine.azCol);
+ break;
+ }
+ case QRF_STYLE_Stats:
+ case QRF_STYLE_StatsEst:
+ case QRF_STYLE_Eqp: {
+ qrfEqpRender(p, 0);
+ qrfWrite(p);
+ break;
+ }
+ }
+ if( p->spec.pzOutput ){
+ if( p->spec.pzOutput[0] ){
+ sqlite3_int64 n, sz;
+ char *zCombined;
+ sz = strlen(p->spec.pzOutput[0]);
+ n = sqlite3_str_length(p->pOut);
+ zCombined = sqlite3_realloc(p->spec.pzOutput[0], sz+n+1);
+ if( zCombined==0 ){
+ sqlite3_free(p->spec.pzOutput[0]);
+ p->spec.pzOutput[0] = 0;
+ qrfOom(p);
+ }else{
+ p->spec.pzOutput[0] = zCombined;
+ memcpy(zCombined+sz, sqlite3_str_value(p->pOut), n+1);
+ }
+ sqlite3_free(sqlite3_str_finish(p->pOut));
+ }else{
+ p->spec.pzOutput[0] = sqlite3_str_finish(p->pOut);
+ }
+ }else if( p->pOut ){
+ sqlite3_free(sqlite3_str_finish(p->pOut));
+ }
+ if( p->expMode>0 ){
+ sqlite3_stmt_explain(p->pStmt, p->expMode-1);
+ }
+ if( p->actualWidth ){
+ sqlite3_free(p->actualWidth);
+ }
+ if( p->pJTrans ){
+ sqlite3 *db = sqlite3_db_handle(p->pJTrans);
+ sqlite3_finalize(p->pJTrans);
+ sqlite3_close(db);
+ }
+}
+
+/*
+** Run the prepared statement pStmt and format the results according
+** to the specification provided in pSpec. Return an error code.
+** If pzErr is not NULL and if an error occurs, write an error message
+** into *pzErr.
+*/
+int sqlite3_format_query_result(
+ sqlite3_stmt *pStmt, /* Statement to evaluate */
+ const sqlite3_qrf_spec *pSpec, /* Format specification */
+ char **pzErr /* Write error message here */
+){
+ Qrf qrf; /* The new Qrf being created */
+
+ if( pStmt==0 ) return SQLITE_OK; /* No-op */
+ if( pSpec==0 ) return SQLITE_MISUSE;
+ qrfInitialize(&qrf, pStmt, pSpec, pzErr);
+ switch( qrf.spec.eStyle ){
+ case QRF_STYLE_Box:
+ case QRF_STYLE_Column:
+ case QRF_STYLE_Markdown:
+ case QRF_STYLE_Table: {
+ /* Columnar modes require that the entire query be evaluated and the
+ ** results stored in memory, so that we can compute column widths */
+ qrfColumnar(&qrf);
+ break;
+ }
+ case QRF_STYLE_Explain: {
+ qrfExplain(&qrf);
+ break;
+ }
+ case QRF_STYLE_StatsVm: {
+ qrfScanStatusVm(&qrf);
+ break;
+ }
+ case QRF_STYLE_Stats:
+ case QRF_STYLE_StatsEst: {
+ qrfEqpStats(&qrf);
+ break;
+ }
+ default: {
+ /* Non-columnar modes where the output can occur after each row
+ ** of result is received */
+ while( qrf.iErr==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
+ qrfOneSimpleRow(&qrf);
+ }
+ break;
+ }
+ }
+ qrfResetStmt(&qrf);
+ qrfFinalize(&qrf);
+ return qrf.iErr;
+}
** lower 30 bits of a 32-bit signed integer.
*/
static int strlen30(const char *z){
- const char *z2 = z;
- while( *z2 ){ z2++; }
- return 0x3fffffff & (int)(z2 - z);
-}
-
-/*
-** Return the length of a string in characters. Multibyte UTF8 characters
-** count as a single character for single-width characters, or as two
-** characters for double-width characters.
-*/
-static int strlenChar(const char *z){
- int n = 0;
- while( *z ){
- if( (0x80&z[0])==0 ){
- n++;
- z++;
- }else{
- int u = 0;
- int len = decodeUtf8((const u8*)z, &u);
- z += len;
- n += cli_wcwidth(u);
- }
- }
- return n;
+ size_t n;
+ if( z==0 ) return 0;
+ n = strlen(z);
- return n>0x3fffffff ? 0x3fffffff : n;
++ return n>0x3fffffff ? 0x3fffffff : (int)n;
}
/*
va_start(ap, zErrMsg);
zMsg = sqlite3_vmprintf(zErrMsg, ap);
va_end(ap);
- sqlite3_fprintf(stderr, "line %lld: %s\n", p->lineno, zMsg);
- exit(1);
+ cli_printf(stderr, "%s %s\n", zLoc, zMsg);
+ cli_exit(1);
+ }
+}
+
+/*
+** Issue an error message from a dot-command.
+*/
+static void dotCmdError(
+ ShellState *p, /* Shell state */
+ int iArg, /* Index of argument on which error occurred */
+ const char *zBrief, /* Brief (<20 character) error description */
+ const char *zDetail, /* Error details */
+ ...
+){
+ FILE *out = stderr;
+ char *zLoc = shellErrorLocation(p);
+ if( zBrief!=0 && iArg>=0 && iArg<p->dot.nArg ){
+ int i = p->dot.aiOfst[iArg];
- int nPrompt = strlen(zBrief) + 5;
++ int nPrompt = strlen30(zBrief) + 5;
+ cli_printf(out, "%s %s\n", zLoc, p->dot.zOrig);
+ if( i > nPrompt ){
+ cli_printf(out, "%s %*s%s ---^\n", zLoc, 1+i-nPrompt, "", zBrief);
+ }else{
+ cli_printf(out, "%s %*s^--- %s\n", zLoc, i, "", zBrief);
+ }
+ }
+ if( zDetail ){
+ char *zMsg;
+ va_list ap;
+ va_start(ap, zDetail);
+ zMsg = sqlite3_vmprintf(zDetail,ap);
+ va_end(ap);
+ cli_printf(out,"%s %s\n", zLoc, zMsg);
+ sqlite3_free(zMsg);
}
+ sqlite3_free(zLoc);
}
+
/*
** SQL function: edit(VALUE)
rc = sqlite3_exec(*pDb, zDedoctor, 0, 0, 0);
rc_err_oom_die(rc);
#endif
- rc = sqlite3_exec(*pDb, zSetReps, 0, 0, 0);
- rc_err_oom_die(rc);
- rc = sqlite3_prepare_v2(*pDb, zRenameRank, -1, &pStmt, 0);
- rc_err_oom_die(rc);
- sqlite3_bind_int(pStmt, 1, nDigits);
- rc = sqlite3_step(pStmt);
- sqlite3_finalize(pStmt);
- if( rc!=SQLITE_DONE ) rc_err_oom_die(SQLITE_NOMEM);
+ rc = sqlite3_exec(*pDb, zSetReps, 0, 0, 0);
+ rc_err_oom_die(rc);
+ rc = sqlite3_prepare_v2(*pDb, zRenameRank, -1, &pStmt, 0);
+ rc_err_oom_die(rc);
+ sqlite3_bind_int(pStmt, 1, nDigits);
+ rc = sqlite3_step(pStmt);
+ sqlite3_finalize(pStmt);
+ if( rc!=SQLITE_DONE ) rc_err_oom_die(SQLITE_NOMEM);
+ }
+ assert(db_int(*pDb, "%s", zHasDupes)==0); /* Consider: remove this */
+ rc = sqlite3_prepare_v2(*pDb, zCollectVar, -1, &pStmt, 0);
+ rc_err_oom_die(rc);
+ rc = sqlite3_step(pStmt);
+ if( rc==SQLITE_ROW ){
+ zColsSpec = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 0));
+ }else{
+ zColsSpec = 0;
+ }
+ if( pzRenamed!=0 ){
+ if( !hasDupes ) *pzRenamed = 0;
+ else{
+ sqlite3_finalize(pStmt);
+ if( SQLITE_OK==sqlite3_prepare_v2(*pDb, zRenamesDone, -1, &pStmt, 0)
+ && SQLITE_ROW==sqlite3_step(pStmt) ){
+ *pzRenamed = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 0));
+ }else
+ *pzRenamed = 0;
+ }
+ }
+ sqlite3_finalize(pStmt);
+ sqlite3_close(*pDb);
+ *pDb = 0;
+ return zColsSpec;
+ }
+}
+
+/*
+** Check if the sqlite_schema table contains one or more virtual tables. If
+** parameter zLike is not NULL, then it is an SQL expression that the
+** sqlite_schema row must also match. If one or more such rows are found,
+** print the following warning to the output:
+**
+** WARNING: Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled
+*/
+static int outputDumpWarning(ShellState *p, const char *zLike){
+ int rc = SQLITE_OK;
+ sqlite3_stmt *pStmt = 0;
+ shellPreparePrintf(p->db, &rc, &pStmt,
+ "SELECT 1 FROM sqlite_schema o WHERE "
+ "sql LIKE 'CREATE VIRTUAL TABLE%%' AND %s", zLike ? zLike : "true"
+ );
+ if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
+ cli_puts("/* WARNING: "
+ "Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled */\n",
+ p->out
+ );
+ }
+ shellFinalize(&rc, pStmt);
+ return rc;
+}
+
+/*
+** Fault-Simulator state and logic.
+*/
+static struct {
+ int iId; /* ID that triggers a simulated fault. -1 means "any" */
+ int iErr; /* The error code to return on a fault */
+ int iCnt; /* Trigger the fault only if iCnt is already zero */
+ int iInterval; /* Reset iCnt to this value after each fault */
+ int eVerbose; /* When to print output */
+ int nHit; /* Number of hits seen so far */
+ int nRepeat; /* Turn off after this many hits. 0 for never */
+ int nSkip; /* Skip this many before first fault */
+} faultsim_state = {-1, 0, 0, 0, 0, 0, 0, 0};
+
+/*
+** This is the fault-sim callback
+*/
+static int faultsim_callback(int iArg){
+ if( faultsim_state.iId>0 && faultsim_state.iId!=iArg ){
+ return SQLITE_OK;
+ }
+ if( faultsim_state.iCnt ){
+ if( faultsim_state.iCnt>0 ) faultsim_state.iCnt--;
+ if( faultsim_state.eVerbose>=2 ){
+ cli_printf(stdout,
+ "FAULT-SIM id=%d no-fault (cnt=%d)\n", iArg, faultsim_state.iCnt);
+ }
+ return SQLITE_OK;
+ }
+ if( faultsim_state.eVerbose>=1 ){
+ cli_printf(stdout,
+ "FAULT-SIM id=%d returns %d\n", iArg, faultsim_state.iErr);
+ }
+ faultsim_state.iCnt = faultsim_state.iInterval;
+ faultsim_state.nHit++;
+ if( faultsim_state.nRepeat>0 && faultsim_state.nRepeat<=faultsim_state.nHit ){
+ faultsim_state.iCnt = -1;
+ }
+ return faultsim_state.iErr;
+}
+
+/*
+** pickStr(zArg, &zErr, zS1, zS2, ..., "");
+**
+** Try to match zArg against zS1, zS2, and so forth until the first
+** emptry string. Return the index of the match or -1 if none is found.
+** If no match is found, and &zErr is not NULL, then write into
+** zErr a message describing the valid choices.
+*/
+static int pickStr(const char *zArg, char **pzErr, ...){
+ int i, n;
+ const char *z;
+ sqlite3_str *pMsg;
+ va_list ap;
+ va_start(ap, pzErr);
+ i = 0;
+ while( (z = va_arg(ap,const char*))!=0 && z[0]!=0 ){
+ if( cli_strcmp(zArg, z)==0 ) return i;
+ i++;
+ }
+ va_end(ap);
+ if( pzErr==0 ) return -1;
+ n = i;
+ pMsg = sqlite3_str_new(0);
+ va_start(ap, pzErr);
+ sqlite3_str_appendall(pMsg, "should be");
+ i = 0;
+ while( (z = va_arg(ap, const char*))!=0 && z[0]!=0 ){
+ if( i==n-1 ){
+ sqlite3_str_append(pMsg,", or",4);
+ }else if( i>0 ){
+ sqlite3_str_append(pMsg, ",", 1);
+ }
+ sqlite3_str_appendf(pMsg, " %s", z);
+ i++;
+ }
+ va_end(ap);
+ *pzErr = sqlite3_str_finish(pMsg);
+ return -1;
+}
+
+/*
+** This function computes what to show the user about the configured
+** titles (or column-names). Output is an integer between 0 and 3:
+**
+** 0: The titles do not matter. Never show anything.
+** 1: Show "--titles off"
+** 2: Show "--titles on"
+** 3: Show "--title VALUE" where VALUE is an encoding method
+** to use, one of: plain sql csv html tcl json
+**
+** Inputs are:
+**
+** spec.bTitles (bT) Whether or not to show the titles
+** spec.eTitle (eT) The actual encoding to be used for titles
+** ModeInfo.bHdr (bH) Default value for spec.bTitles
+** ModeInfo.eHdr (eH) Default value for spec.eTitle
+** bAll Whether the -v option is used
+*/
+static int modeTitleDsply(ShellState *p, int bAll){
+ int eMode = p->mode.eMode;
+ const ModeInfo *pI = &aModeInfo[eMode];
+ int bT = p->mode.spec.bTitles;
+ int eT = p->mode.spec.eTitle;
+ int bH = pI->bHdr;
+ int eH = pI->eHdr;
+
+ /* Variable "v" is the truth table that will determine the answer
+ **
+ ** Actual encoding is different from default
+ ** vvvvvvvv */
+ sqlite3_uint64 v = 0x0133013311220102;
+ /* ^^^^ ^^^^
+ ** Upper 2-byte groups for when ON/OFF disagrees with
+ ** the default. */
+
+ if( bH==0 ) return 0; /* Header not appliable. Ex: off, count */
+
+ if( eT==0 ) eT = eH; /* Fill in missing spec.eTitle */
+ if( bT==0 ) bT = bH; /* Fill in missing spec.bTitles */
+
+ if( eT!=eH ) v >>= 32; /* Encoding disagree in upper 4-bytes */
+ if( bT!=bH ) v >>= 16; /* ON/OFF disagree in upper 2-byte pairs */
+ if( bT<2 ) v >>= 8; /* ON in even bytes, OFF in odd bytes (1st byte 0) */
+ if( !bAll ) v >>= 4; /* bAll values are in the lower half-byte */
+
+ return v & 3; /* Return the selected truth-table entry */
+}
+
+/*
+** DOT-COMMAND: .mode
+**
+** USAGE: .mode [MODE] [OPTIONS]
+**
+** Change the output mode to MODE and/or apply OPTIONS to the
+** output mode. If no arguments, show the current output mode
+** and relevant options.
+**
+** Options:
+**
+** --align STRING Set the alignment of text in columnar modes
+** String consists of characters 'L', 'C', 'R'
+** meaning "left", "centered", and "right", with
+** one letter per column starting from the left.
+** Unspecified alignment defaults to 'L'.
+**
+** --charlimit N Set the maximum number of output characters to
+** show for any single SQL value to N. Longer values
+** truncated. Zero means "no limit".
+**
+** --colsep STRING Use STRING as the column separator
+**
+** --escape ESC Enable/disable escaping of control characters
+** in output. ESC can be "off", "ascii", or
+** "symbol".
+**
+** --linelimit N Set the maximum number of output lines to show for
+** any single SQL value to N. Longer values are
+** truncated. Zero means "no limit". Only works
+** in "line" mode and in columnar modes.
+**
+** --null STRING Render SQL NULL values as the given string
+**
+** --quote ARG Enable/disable quoting of text. ARG can be
+** "off", "on", "sql", "csv", "html", "tcl",
+** or "json". "off" means show the text as-is.
+** "on and "sql" are synonyms.
+**
+** --reset Changes all mode settings back to their default.
+**
+** --rowsep STRING Use STRING as the row separator
+**
+** --screenwidth N Declare the screen width of the output device
+** to be N characters. An attempt may be made to
+** wrap output text to fit within this limit. Zero
+** means "no limit". Or N can be "auto" to set the
+** width automatically.
+**
+** --tablename NAME Set the name of the table for "insert" mode.
+**
+** --textjsonb BOOLEAN If enabled, JSONB text is displayed as text JSON.
+**
+** --title ARG Whether or not to show column headers, and if so
+** how to encode them. ARG can be "off", "on",
+** "sql", "csv", "html", "tcl", or "json".
+**
+** -v|--verbose Verbose output
+**
+** --widths LIST Set the columns widths for columnar modes. The
+** argument is a list of integers, one for each
+** column. A "0" width means use a dynamic width
+** based on the actual width of data. If there are
+** fewer entries in LIST than columns, "0" is used
+** for the unspecified widths.
+**
+** --wordwrap BOOLEAN Enable/disable word wrapping
+**
+** --wrap N Wrap columns wider than N characters
+**
+** --ww Shorthand for "--wordwrap on"
+*/
+static int dotCmdMode(ShellState *p){
+ int nArg = p->dot.nArg; /* Number of arguments */
+ char **azArg = p->dot.azArg;/* Argument list */
+ int eMode = -1; /* New mode value, or -1 for none */
+ int iMode = -1; /* Index of the argument that is the mode name */
+ int i; /* Loop counter */
+ int k; /* Misc index variable */
+ int chng = 0; /* True if anything has changed */
+ int bAll = 0; /* Show all details of the mode */
+
+ for(i=1; i<nArg; i++){
+ const char *z = azArg[i];
+ if( z[0]=='-' && z[1]=='-' ) z++;
+ if( z[0]!='-'
+ && iMode<0
+ && (eMode = modeFind(azArg[i]))>=0
+ && eMode!=MODE_Www
+ ){
+ iMode = i;
+ modeChange(&p->mode, eMode);
+ /* (Legacy) If the mode is MODE_Insert and the next argument
+ ** is not an option, then the next argument must be the table
+ ** name.
+ */
+ if( i+1<nArg && azArg[i+1][0]!='-' ){
+ i++;
+ modeSetStr(&p->mode.spec.zTableName, azArg[i]);
+ }
+ chng = 1;
+ }else if( optionMatch(z,"align") ){
+ char *zAlign;
+ int nAlign;
+ int nErr = 0;
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ i++;
+ zAlign = azArg[i];
+ nAlign = 0x3fff & strlen(zAlign);
+ free(p->mode.spec.aAlign);
+ p->mode.spec.aAlign = malloc(nAlign);
+ shell_check_oom(p->mode.spec.aAlign);
+ for(k=0; k<nAlign; k++){
+ unsigned char c = 0;
+ switch( zAlign[k] ){
+ case 'l': case 'L': c = QRF_ALIGN_Left; break;
+ case 'c': case 'C': c = QRF_ALIGN_Center; break;
+ case 'r': case 'R': c = QRF_ALIGN_Right; break;
+ default: nErr++; break;
+ }
+ p->mode.spec.aAlign[k] = c;
+ }
+ p->mode.spec.nAlign = nAlign;
+ chng = 1;
+ if( nErr ){
+ dotCmdError(p, i, "bad alignment string",
+ "Should contain only characters L, C, and R.");
+ return 1;
+ }
+ }else if( 0<=(k=pickStr(z,0,"-charlimit","-linelimit","")) ){
+ int w; /* 0 1 */
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ w = integerValue(azArg[++i]);
+ if( k==0 ){
+ p->mode.spec.nCharLimit = w;
+ }else{
+ p->mode.spec.nLineLimit = 2;
+ }
+ chng = 1;
+ }else if( 0<=(k=pickStr(z,0,"-tablename","-rowsep","-colsep","-null","")) ){
+ /* 0 1 2 3 */
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ i++;
+ switch( k ){
+ case 0: modeSetStr(&p->mode.spec.zTableName, azArg[i]); break;
+ case 1: modeSetStr(&p->mode.spec.zRowSep, azArg[i]); break;
+ case 2: modeSetStr(&p->mode.spec.zColumnSep, azArg[i]); break;
+ case 3: modeSetStr(&p->mode.spec.zNull, azArg[i]); break;
+ }
+ chng = 1;
+ }else if( optionMatch(z,"escape") ){
+ /* See similar code at tag-20250224-1 */
+ char *zErr = 0;
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ i++; /* 0 1 2 <-- One less than QRF_ESC_ */
+ k = pickStr(azArg[i],&zErr,"off","ascii","symbol","");
+ if( k<0 ){
+ dotCmdError(p, i, "unknown escape type", "%s", zErr);
+ sqlite3_free(zErr);
+ return 1;
+ }
+ p->mode.spec.eEsc = k+1;
+ chng = 1;
+ }else if( optionMatch(z,"quote") ){
+ if( i+1<nArg
+ && azArg[i+1][0]!='-'
+ && (iMode>0 || modeFind(azArg[i+1])<0)
+ ){
+ /* --quote is followed by an argument other that is not an option
+ ** or a mode name. See it must be a boolean or a keyword to describe
+ ** how to set quoting. */
+ i++;
+ if( (k = pickStr(azArg[i],0,"no","yes","0","1",""))>=0 ){
+ k &= 1; /* 0 for "off". 1 for "on". */
+ }else{
+ char *zErr = 0; /* 0 1 2 3 4 5 6 */
+ k = pickStr(azArg[i],&zErr,"off","on","sql","csv","html","tcl","json",
+ "");
+ if( k<0 ){
+ dotCmdError(p, i, "unknown", "%z", zErr);
+ return 1;
+ }
+ }
+ }else{
+ /* (Legacy) no following boolean argument. Turn quoting on */
+ k = 1;
+ }
+ switch( k ){
+ case 1: /* on */
+ case 2: /* sql */
+ p->mode.spec.eText = QRF_TEXT_Sql;
+ p->mode.spec.eBlob = QRF_BLOB_Sql;
+ break;
+ case 3: /* csv */
+ p->mode.spec.eText = QRF_TEXT_Csv;
+ p->mode.spec.eBlob = QRF_BLOB_Text;
+ break;
+ case 4: /* html */
+ p->mode.spec.eText = QRF_TEXT_Html;
+ p->mode.spec.eBlob = QRF_BLOB_Text;
+ break;
+ case 5: /* tcl */
+ p->mode.spec.eText = QRF_TEXT_Tcl;
+ p->mode.spec.eBlob = QRF_BLOB_Text;
+ break;
+ case 6: /* json */
+ p->mode.spec.eText = QRF_TEXT_Json;
+ p->mode.spec.eBlob = QRF_BLOB_Json;
+ break;
+ default: /* off */
+ p->mode.spec.eText = QRF_TEXT_Plain;
+ p->mode.spec.eBlob = QRF_BLOB_Text;
+ break;
+ }
+ chng = 1;
+ }else if( optionMatch(z,"noquote") ){
+ /* (undocumented legacy) --noquote always turns quoting off */
+ p->mode.spec.eText = QRF_TEXT_Plain;
+ p->mode.spec.eBlob = QRF_BLOB_Text;
+ chng = 1;
+ }else if( optionMatch(z,"reset") ){
- int eMode = p->mode.eMode;
++ int saved_eMode = p->mode.eMode;
+ modeFree(&p->mode);
- modeChange(&p->mode, eMode);
++ modeChange(&p->mode, saved_eMode);
+ }else if( optionMatch(z,"screenwidth") ){
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ k = pickStr(azArg[i+1],0,"off","auto","");
+ if( k==0 ){
+ p->mode.bAutoScreenWidth = 0;
+ p->mode.spec.nScreenWidth = 0;
+ }else if( k==1 ){
+ p->mode.bAutoScreenWidth = 1;
+ }else{
+ i64 w = integerValue(azArg[i+1]);
+ p->mode.bAutoScreenWidth = 0;
+ if( w<0 ) w = 0;
+ if( w>QRF_MAX_WIDTH ) w = QRF_MAX_WIDTH;
+ p->mode.spec.nScreenWidth = w;
+ }
+ i++;
+ chng = 1;
+ }else if( optionMatch(z,"textjsonb") ){
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ p->mode.spec.bTextJsonb = booleanValue(azArg[++i]) ? QRF_Yes : QRF_No;
+ chng = 1;
+ }else if( optionMatch(z,"titles") ){
+ char *zErr = 0;
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ k = pickStr(azArg[++i],&zErr,
+ "off","on","plain","sql","csv","html","tcl","json","");
+ /* 0 1 2 3 4 5 6 7 */
+ if( k<0 ){
+ dotCmdError(p, i, "bad --titles value","%z", zErr);
+ return 1;
+ }
+ p->mode.spec.bTitles = k>=1 ? QRF_Yes : QRF_No;
+ p->mode.spec.eTitle = k>1 ? k-1 : aModeInfo[p->mode.eMode].eHdr;
+ chng = 1;
- }else if( optionMatch(z,"widths") ){
++ }else if( optionMatch(z,"widths") || optionMatch(z,"width") ){
+ int nWidth = 0;
+ short int *aWidth;
- const char *z;
++ const char *zW;
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
- z = azArg[++i];
++ zW = azArg[++i];
+ /* Every width value takes at least 2 bytes in the input string to
- ** specify, so strlen(z) bytes should be plenty of space to hold the
++ ** specify, so strlen(zW) bytes should be plenty of space to hold the
+ ** result. */
- aWidth = malloc( strlen(z) );
- while( isspace(z[0]) ) z++;
- while( z[0] ){
++ aWidth = malloc( strlen(zW) );
++ while( isspace(zW[0]) ) zW++;
++ while( zW[0] ){
+ int w = 0;
- k = z[0]=='-' && isdigit(z[1]);
- while( isdigit(z[k]) ){
- w = w*10 + z[k] - '0';
++ k = zW[0]=='-' && isdigit(zW[1]);
++ while( isdigit(zW[k]) ){
++ w = w*10 + zW[k] - '0';
+ if( w>QRF_MAX_WIDTH ){
+ dotCmdError(p,i+1,"width too big",
+ "Maximum column width is %d", QRF_MAX_WIDTH);
+ free(aWidth);
+ return 1;
+ }
+ k++;
+ }
- if( z[0]=='-' ) w = -w;
++ if( zW[0]=='-' ) w = -w;
+ aWidth[nWidth++] = w;
- z += k;
- if( z[0]==',' ) z++;
- while( isspace(z[0]) ) z++;
++ zW += k;
++ if( zW[0]==',' ) zW++;
++ while( isspace(zW[0]) ) zW++;
+ }
+ free(p->mode.spec.aWidth);
+ p->mode.spec.aWidth = aWidth;
+ p->mode.spec.nWidth = nWidth;
+ chng = 1;
+ }else if( optionMatch(z,"wrap") ){
+ int w;
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ w = integerValue(azArg[++i]);
+ if( w<(-QRF_MAX_WIDTH) ) w = -QRF_MAX_WIDTH;
+ if( w>QRF_MAX_WIDTH ) w = QRF_MAX_WIDTH;
+ p->mode.spec.nWrap = w;
+ chng = 1;
+ }else if( optionMatch(z,"ww") ){
+ p->mode.spec.bWordWrap = QRF_Yes;
+ chng = 1;
+ }else if( optionMatch(z,"wordwrap") ){
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ p->mode.spec.bWordWrap = (u8)booleanValue(azArg[++i]) ? QRF_Yes : QRF_No;
+ chng = 1;
+ }else if( optionMatch(z,"v") || optionMatch(z,"verbose") ){
+ bAll = 1;
+ }else if( z[0]=='-' ){
+ dotCmdError(p, i, "bad option", "Use \".help .mode\" for more info");
+ return 1;
+ }else{
+ dotCmdError(p, i, iMode>0?"bad argument":"unknown mode",
+ "Use \".help .mode\" for more info");
+ return 1;
+ }
+ }
+ if( !chng || bAll ){
+ const ModeInfo *pI = aModeInfo + p->mode.eMode;
+ sqlite3_str *pDesc = sqlite3_str_new(p->db);
+ char *zDesc;
+ const char *zSetting;
+
+ sqlite3_str_appendall(pDesc,pI->zName);
+ if( bAll || (p->mode.spec.nAlign && pI->eCx==2) ){
- int i;
++ int ii;
+ sqlite3_str_appendall(pDesc, " --align \"");
- for(i=0; i<p->mode.spec.nAlign; i++){
- unsigned char a = p->mode.spec.aAlign[i];
++ for(ii=0; ii<p->mode.spec.nAlign; ii++){
++ unsigned char a = p->mode.spec.aAlign[ii];
+ sqlite3_str_appendchar(pDesc, 1, "LLCR"[a&3]);
+ }
+ sqlite3_str_append(pDesc, "\"", 1);
+ }
+ if( bAll || p->mode.spec.nCharLimit>0 ){
+ sqlite3_str_appendf(pDesc, " --charlimit %d",p->mode.spec.nCharLimit);
+ }
+ zSetting = aModeStr[pI->eCSep];
+ if( bAll || (zSetting && cli_strcmp(zSetting,p->mode.spec.zColumnSep)!=0) ){
+ sqlite3_str_appendf(pDesc, " --colsep ");
+ append_c_string(pDesc, p->mode.spec.zColumnSep);
+ }
+ if( bAll || p->mode.spec.eEsc!=QRF_Auto ){
+ sqlite3_str_appendf(pDesc, " --escape %s",qrfEscNames[p->mode.spec.eEsc]);
+ }
+ if( bAll || (p->mode.spec.nLineLimit>0 && pI->eCx>0) ){
+ sqlite3_str_appendf(pDesc, " --linelimit %d",p->mode.spec.nLineLimit);
+ }
+ zSetting = aModeStr[pI->eNull];
+ if( bAll || (zSetting && cli_strcmp(zSetting,p->mode.spec.zNull)!=0) ){
+ sqlite3_str_appendf(pDesc, " --null ");
+ append_c_string(pDesc, p->mode.spec.zNull);
+ }
+ if( bAll
+ || (pI->eText!=p->mode.spec.eText && (pI->eText>1 || p->mode.spec.eText>1))
+ ){
+ sqlite3_str_appendf(pDesc," --quote %s",qrfQuoteNames[p->mode.spec.eText]);
+ }
+ zSetting = aModeStr[pI->eRSep];
+ if( bAll || (zSetting && cli_strcmp(zSetting,p->mode.spec.zRowSep)!=0) ){
+ sqlite3_str_appendf(pDesc, " --rowsep ");
+ append_c_string(pDesc, p->mode.spec.zRowSep);
+ }
+ if( bAll
+ || (pI->eCx && (p->mode.spec.nScreenWidth>0 || p->mode.bAutoScreenWidth))
+ ){
+ if( p->mode.bAutoScreenWidth ){
+ sqlite3_str_appendall(pDesc, " --screenwidth auto");
+ }else{
+ sqlite3_str_appendf(pDesc," --screenwidth %d",
+ p->mode.spec.nScreenWidth);
+ }
+ }
+ if( bAll || p->mode.eMode==MODE_Insert ){
+ sqlite3_str_appendf(pDesc," --tablename ");
+ append_c_string(pDesc, p->mode.spec.zTableName);
+ }
+ if( bAll || p->mode.spec.bTextJsonb ){
+ sqlite3_str_appendf(pDesc," --textjsonb %s",
+ p->mode.spec.bTextJsonb==QRF_Yes ? "on" : "off");
+ }
+ k = modeTitleDsply(p, bAll);
+ if( k==1 ){
+ sqlite3_str_appendall(pDesc, " --titles off");
+ }else if( k==2 ){
+ sqlite3_str_appendall(pDesc, " --titles on");
+ }else if( k==3 ){
+ static const char *azTitle[] =
+ { "plain", "sql", "csv", "html", "tcl", "json"};
+ sqlite3_str_appendf(pDesc, " --titles %s",
+ azTitle[p->mode.spec.eTitle-1]);
+ }
+ if( p->mode.spec.nWidth>0 && (bAll || pI->eCx==2) ){
- int i;
++ int ii;
+ const char *zSep = " --widths ";
- for(i=0; i<p->mode.spec.nWidth; i++){
- sqlite3_str_appendf(pDesc, "%s%d", zSep, (int)p->mode.spec.aWidth[i]);
++ for(ii=0; ii<p->mode.spec.nWidth; ii++){
++ sqlite3_str_appendf(pDesc, "%s%d", zSep, (int)p->mode.spec.aWidth[ii]);
+ zSep = ",";
+ }
+ }else if( bAll ){
+ sqlite3_str_appendall(pDesc, " --widths \"\"");
+ }
+ if( bAll || (pI->eCx>0 && p->mode.spec.bWordWrap) ){
+ if( bAll ){
+ sqlite3_str_appendf(pDesc, " --wordwrap %s",
+ p->mode.spec.bWordWrap==QRF_Yes ? "on" : "off");
+ }
+ if( p->mode.spec.nWrap ){
+ sqlite3_str_appendf(pDesc, " --wrap %d", p->mode.spec.nWrap);
+ }
+ if( !bAll ) sqlite3_str_append(pDesc, " --ww", 5);
+ }
+ zDesc = sqlite3_str_finish(pDesc);
+ cli_printf(p->out, "current output mode: %s\n", zDesc);
+ sqlite3_free(zDesc);
+ }
+ return 0;
+}
+
+/*
+** DOT-COMMAND: .output [OPTIONS] [FILE]
+** ONELINER: Redirect output
+**
+** Begin redirecting output to FILE. Or if FILE is omitted, revert
+** to sending output to the console. If FILE begins with "|" then
+** the remainder of file is taken as a pipe and output is directed
+** into that pipe. If FILE is "memory" then output is captured in an
+** internal memory buffer. If FILE is "off" then output is redirected
+** into /dev/null or the equivalent.
+**
+** Options:
+**
+** --bom Prepend a byte-order mark to the output
+**
+** -e Accumulate output in a temporary text file then
+** launch a text editor when the redirection ends.
+**
+** --error-prefix X Use X as the left-margin prefix for error messages.
+** Set to an empty string to restore the default.
+**
+** --glob GLOB Raise an error if the memory buffer does not match
+** the GLOB pattern.
+**
+** --keep Continue using the same "memory" buffer. Do not
+** reset it or delete it. Useful in combination with
+** --glob, --not-glob, and/or --verify.
+**
+** ---notglob GLOB Raise an error if the memory buffer does not match
+** the GLOB pattern.
+**
+** --plain Use plain text rather than HTML tables with -w
+**
+** --show Write the memory buffer to the screen, for debugging.
+**
+** --verify ENDMARK Read subsequent lines of text until the first line
+** that matches ENDMARK. Discard the ENDMARK. Compare
+** the text against the accumulated output in memory and
+** raise an error if there are any differences.
+**
+** -w Show the output in a web browser. Output is
+** written into a temporary HTML file until the
+** redirect ends, then the web browser is launched.
+** Query results are shown as HTML tables, unless
+** the --plain is used too.
+**
+** -x Show the output in a spreadsheet. Output is
+** written to a temp file as CSV then the spreadsheet
+** is launched when
+**
+** DOT-COMMAND: .once [OPTIONS] FILE ...
+** ONELINER: Redirect output for the next SQL statement or dot-command
+**
+** Write the output for the next line of SQL or the next dot-command into
+** FILE. If FILE begins with "|" then it is a program into which output
+** is written. The FILE argument should be omitted if one of the -e, -w,
+** or -x options is used.
+**
+** Options:
+**
+** -e Capture output into a temporary file then bring up
+** a text editor on that temporary file.
+**
+** --plain Use plain text rather than HTML tables with -w
+**
+** -w Capture output into an HTML file then bring up that
+** file in a web browser
+**
+** -x Show the output in a spreadsheet. Output is
+** written to a temp file as CSV then the spreadsheet
+** is launched when
+**
+** DOT-COMMAND: .excel
+** ONELINER: Display results of the next SQL statement in a spreadsheet
+**
+** Shorthand for ".once -x"
+**
+** DOT-COMMAND: .www [--plain]
+** ONELINER: Display result of the next SQL statement in a web browser
+**
+** Shorthand for ".once -w" or ".once --plain -w"
+*/
+static int dotCmdOutput(ShellState *p){
+ int nArg = p->dot.nArg; /* Number of arguments */
+ char **azArg = p->dot.azArg; /* Text of the arguments */
+ char *zFile = 0; /* The FILE argument */
+ int i; /* Loop counter */
+ int eMode = 0; /* 0: .outout/.once, 'x'=.excel, 'w'=.www */
+ int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */
+ int bPlain = 0; /* --plain option */
+ int bKeep = 0; /* --keep option */
+ char *zCheck = 0; /* Argument to --glob, --notglob, --verify */
+ int eCheck = 0; /* 1: --glob, 2: --notglob, 3: --verify */
+ static const char *zBomUtf8 = "\357\273\277";
+ const char *zBom = 0;
+ char c = azArg[0][0];
+ int n = strlen30(azArg[0]);
+
+ failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]);
+ if( c=='e' ){
+ eMode = 'x';
+ bOnce = 2;
+ }else if( c=='w' ){
+ eMode = 'w';
+ bOnce = 2;
+ }else if( n>=2 && cli_strncmp(azArg[0],"once",n)==0 ){
+ bOnce = 1;
+ }
+ for(i=1; i<nArg; i++){
+ char *z = azArg[i];
+ if( z[0]=='-' ){
+ if( z[1]=='-' ) z++;
+ if( cli_strcmp(z,"-bom")==0 ){
+ zBom = zBomUtf8;
+ }else if( cli_strcmp(z,"-plain")==0 ){
+ bPlain = 1;
+ }else if( c=='o' && z[0]=='1' && z[1]!=0 && z[2]==0
+ && (z[1]=='x' || z[1]=='e' || z[1]=='w') ){
+ if( bKeep || eMode || eCheck ){
+ dotCmdError(p, i, "incompatible with prior options",0);
+ goto dotCmdOutput_error;
+ }
+ eMode = z[1];
+ }else if( cli_strcmp(z,"-keep")==0 ){
+ bKeep = 1;
+ }else if( cli_strcmp(z,"-show")==0 ){
+ if( cli_output_capture ){
+ sqlite3_fprintf(stdout, "%s", sqlite3_str_value(cli_output_capture));
+ }
+ bKeep = 1;
+ }else if( cli_strcmp(z,"-glob")==0
+ || cli_strcmp(z,"-notglob")==0
+ || cli_strcmp(z,"-verify")==0
+ ){
+ if( eCheck || eMode ){
+ dotCmdError(p, i, "incompatible with prior options",0);
+ goto dotCmdOutput_error;
+ }
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ goto dotCmdOutput_error;
+ }
+ zCheck = azArg[++i];
+ eCheck = z[1]=='g' ? 1 : z[1]=='n' ? 2 : 3;
+ }else if( optionMatch(z,"error-prefix") ){
+ if( i+1>=nArg ){
+ dotCmdError(p, i, "missing argument", 0);
+ return 1;
+ }
+ free(p->zErrPrefix);
+ i++;
+ p->zErrPrefix = azArg[i][0]==0 ? 0 : strdup(azArg[i]);
+ }else{
+ dotCmdError(p, i, "unknown option", 0);
+ sqlite3_free(zFile);
+ return 1;
+ }
+ }else if( zFile==0 && eMode==0 ){
+ if( bKeep || eCheck ){
+ dotCmdError(p, i, "incompatible with prior options",0);
+ goto dotCmdOutput_error;
+ }
+ if( cli_strcmp(z, "memory")==0 && bOnce ){
+ dotCmdError(p, 0, "cannot redirect to \"memory\"", 0);
+ goto dotCmdOutput_error;
+ }
+ if( cli_strcmp(z, "off")==0 ){
+#ifdef _WIN32
+ zFile = sqlite3_mprintf("nul");
+#else
+ zFile = sqlite3_mprintf("/dev/null");
+#endif
+ }else{
+ zFile = sqlite3_mprintf("%s", z);
+ }
+ if( zFile && zFile[0]=='|' ){
+ while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
+ break;
+ }
+ }else{
+ dotCmdError(p, i, "surplus argument", 0);
+ sqlite3_free(zFile);
+ return 1;
+ }
+ }
+ if( zFile==0 && !bKeep ){
+ zFile = sqlite3_mprintf("stdout");
+ shell_check_oom(zFile);
+ }
+ if( bOnce ){
+ p->outCount = 2;
+ }else{
+ p->outCount = 0;
+ }
+ if( eCheck ){
+ char *zTest;
+ if( cli_output_capture ){
+ zTest = sqlite3_str_value(cli_output_capture);
+ }else{
+ zTest = "";
+ }
+ p->nTestRun++;
+ if( eCheck==3 ){
+ int nCheck = strlen30(zCheck);
+ sqlite3_str *pPattern = sqlite3_str_new(p->db);
+ char *zPattern;
+ sqlite3_int64 iStart = p->lineno;
+ char zLine[2000];
+ while( sqlite3_fgets(zLine,sizeof(zLine),p->in) ){
+ if( strchr(zLine,'\n') ) p->lineno++;
+ if( cli_strncmp(zCheck,zLine,nCheck)==0 ) break;
+ sqlite3_str_appendall(pPattern, zLine);
+ }
+ zPattern = sqlite3_str_finish(pPattern);
+ if( cli_strcmp(zPattern,zTest)!=0 ){
+ sqlite3_fprintf(stderr,
+ "%s:%lld: --verify does matches prior output\n",
+ p->zInFile, iStart);
+ p->nTestErr++;
+ }
+ sqlite3_free(zPattern);
+ }else{
+ char *zGlob = sqlite3_mprintf("*%s*", zCheck);
+ if( eCheck==1 && sqlite3_strglob(zGlob, zTest)!=0 ){
+ sqlite3_fprintf(stderr,
+ "%s:%lld: --glob \"%s\" does not match prior output\n",
+ p->zInFile, p->lineno, zCheck);
+ p->nTestErr++;
+ }else if( eCheck==2 && sqlite3_strglob(zGlob, zTest)==0 ){
+ sqlite3_fprintf(stderr,
+ "%s:%lld: --notglob \"%s\" matches prior output\n",
+ p->zInFile, p->lineno, zCheck);
+ p->nTestErr++;
+ }
+ sqlite3_free(zGlob);
+ }
+ }
+ if( !bKeep ) output_reset(p);
+#ifndef SQLITE_NOHAVE_SYSTEM
+ if( eMode=='e' || eMode=='x' || eMode=='w' ){
+ p->doXdgOpen = 1;
+ outputModePush(p);
+ if( eMode=='x' ){
+ /* spreadsheet mode. Output as CSV. */
+ newTempFile(p, "csv");
+ p->mode.bEcho = 0;
+ p->mode.eMode = MODE_Csv;
+ modeSetStr(&p->mode.spec.zColumnSep, SEP_Comma);
+ modeSetStr(&p->mode.spec.zRowSep, SEP_CrLf);
+#ifdef _WIN32
+ zBom = zBomUtf8; /* Always include the BOM on Windows, as Excel does
+ ** not work without it. */
+#endif
+ }else if( eMode=='w' ){
+ /* web-browser mode. */
+ newTempFile(p, "html");
+ if( !bPlain ) p->mode.eMode = MODE_Www;
+ }else{
+ /* text editor mode */
+ newTempFile(p, "txt");
}
- assert(db_int(*pDb, "%s", zHasDupes)==0); /* Consider: remove this */
- rc = sqlite3_prepare_v2(*pDb, zCollectVar, -1, &pStmt, 0);
- rc_err_oom_die(rc);
- rc = sqlite3_step(pStmt);
- if( rc==SQLITE_ROW ){
- zColsSpec = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 0));
+ sqlite3_free(zFile);
+ zFile = sqlite3_mprintf("%s", p->zTempFile);
+ }
+#endif /* SQLITE_NOHAVE_SYSTEM */
+ if( !bKeep ) shell_check_oom(zFile);
+ if( bKeep ){
+ /* no-op */
+ }else if( cli_strcmp(zFile,"memory")==0 ){
+ if( cli_output_capture ){
+ sqlite3_free(sqlite3_str_finish(cli_output_capture));
+ }
+ cli_output_capture = sqlite3_str_new(0);
+ }else if( zFile[0]=='|' ){
+#ifdef SQLITE_OMIT_POPEN
+ eputz("Error: pipes are not supported in this OS\n");
+ output_redir(p, stdout);
+ goto dotCmdOutput_error;
+#else
+ FILE *pfPipe = sqlite3_popen(zFile + 1, "w");
+ if( pfPipe==0 ){
+ assert( stderr!=NULL );
+ cli_printf(stderr,"Error: cannot open pipe \"%s\"\n", zFile + 1);
+ goto dotCmdOutput_error;
}else{
- zColsSpec = 0;
+ output_redir(p, pfPipe);
+ if( zBom ) cli_puts(zBom, pfPipe);
+ sqlite3_snprintf(sizeof(p->outfile), p->outfile, "%s", zFile);
}
- if( pzRenamed!=0 ){
- if( !hasDupes ) *pzRenamed = 0;
- else{
- sqlite3_finalize(pStmt);
- if( SQLITE_OK==sqlite3_prepare_v2(*pDb, zRenamesDone, -1, &pStmt, 0)
- && SQLITE_ROW==sqlite3_step(pStmt) ){
- *pzRenamed = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 0));
- }else
- *pzRenamed = 0;
+#endif
+ }else{
+ FILE *pfFile = output_file_open(zFile);
+ if( pfFile==0 ){
+ if( cli_strcmp(zFile,"off")!=0 ){
+ assert( stderr!=NULL );
+ cli_printf(stderr,"Error: cannot write to \"%s\"\n", zFile);
+ }
+ goto dotCmdOutput_error;
+ } else {
+ output_redir(p, pfFile);
+ if( zBom ) cli_puts(zBom, pfFile);
+ if( bPlain && eMode=='w' ){
+ cli_puts(
+ "<!DOCTYPE html>\n<BODY>\n<PLAINTEXT>\n",
+ pfFile
+ );
}
+ sqlite3_snprintf(sizeof(p->outfile), p->outfile, "%s", zFile);
}
- sqlite3_finalize(pStmt);
- sqlite3_close(*pDb);
- *pDb = 0;
- return zColsSpec;
}
-}
+ sqlite3_free(zFile);
+ return 0;
-/*
-** Check if the sqlite_schema table contains one or more virtual tables. If
-** parameter zLike is not NULL, then it is an SQL expression that the
-** sqlite_schema row must also match. If one or more such rows are found,
-** print the following warning to the output:
-**
-** WARNING: Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled
-*/
-static int outputDumpWarning(ShellState *p, const char *zLike){
- int rc = SQLITE_OK;
- sqlite3_stmt *pStmt = 0;
- shellPreparePrintf(p->db, &rc, &pStmt,
- "SELECT 1 FROM sqlite_schema o WHERE "
- "sql LIKE 'CREATE VIRTUAL TABLE%%' AND %s", zLike ? zLike : "true"
- );
- if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
- sqlite3_fputs("/* WARNING: "
- "Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled */\n",
- p->out
- );
- }
- shellFinalize(&rc, pStmt);
- return rc;
+dotCmdOutput_error:
+ sqlite3_free(zFile);
+ return 1;
}
--
/*
- ** If an input line begins with "." then invoke this routine to
- ** process that line.
- **
- ** Return 1 on error, 2 to exit, and 0 otherwise.
-** Fault-Simulator state and logic.
++** Parse input line zLine up into individual arguments. Retain the
++** parse in the p->dot substructure.
*/
- static int do_meta_command(const char *zLine, ShellState *p){
-static struct {
- int iId; /* ID that triggers a simulated fault. -1 means "any" */
- int iErr; /* The error code to return on a fault */
- int iCnt; /* Trigger the fault only if iCnt is already zero */
- int iInterval; /* Reset iCnt to this value after each fault */
- int eVerbose; /* When to print output */
- int nHit; /* Number of hits seen so far */
- int nRepeat; /* Turn off after this many hits. 0 for never */
- int nSkip; /* Skip this many before first fault */
-} faultsim_state = {-1, 0, 0, 0, 0, 0, 0, 0};
++static void parseDotCmdArgs(const char *zLine, ShellState *p){
++ char *z;
+ int h = 1;
+ int nArg = 0;
- int n, c;
- int rc = 0;
- char *z;
- char **azArg;
- #if !defined(SQLITE_OMIT_VIRTUALTABLE) && !defined(SQLITE_OMIT_AUTHORIZATION)
- if( p->expert.pExpert ){
- expertFinish(p, 1, 0);
-/*
-** This is the fault-sim callback
-*/
-static int faultsim_callback(int iArg){
- if( faultsim_state.iId>0 && faultsim_state.iId!=iArg ){
- return SQLITE_OK;
-- }
- #endif
-
- /* Parse the input line into tokens.
- */
- if( faultsim_state.iCnt ){
- if( faultsim_state.iCnt>0 ) faultsim_state.iCnt--;
- if( faultsim_state.eVerbose>=2 ){
- sqlite3_fprintf(stdout,
- "FAULT-SIM id=%d no-fault (cnt=%d)\n", iArg, faultsim_state.iCnt);
+ p->dot.zOrig = zLine;
+ free(p->dot.zCopy);
+ z = p->dot.zCopy = strdup(zLine);
+ shell_check_oom(z);
- nArg = 0;
+ while( z[h] ){
+ while( IsSpace(z[h]) ){ h++; }
+ if( z[h]==0 ) break;
+ if( nArg+2>p->dot.nAlloc ){
+ p->dot.nAlloc = nArg+22;
+ p->dot.azArg = realloc(p->dot.azArg,p->dot.nAlloc*sizeof(char*));
+ shell_check_oom(p->dot.azArg);
+ p->dot.aiOfst = realloc(p->dot.aiOfst,p->dot.nAlloc*sizeof(int));
+ shell_check_oom(p->dot.aiOfst);
+ p->dot.abQuot = realloc(p->dot.abQuot,p->dot.nAlloc);
+ shell_check_oom(p->dot.abQuot);
+ }
+ if( z[h]=='\'' || z[h]=='"' ){
+ int delim = z[h++];
+ p->dot.abQuot[nArg] = 1;
+ p->dot.azArg[nArg] = &z[h];
+ p->dot.aiOfst[nArg] = h;
+ while( z[h] && z[h]!=delim ){
+ if( z[h]=='\\' && delim=='"' && z[h+1]!=0 ) h++;
+ h++;
+ }
+ if( z[h]==delim ){
+ z[h++] = 0;
+ }
+ if( delim=='"' ) resolve_backslashes(p->dot.azArg[nArg]);
+ }else{
+ p->dot.abQuot[nArg] = 0;
+ p->dot.azArg[nArg] = &z[h];
+ p->dot.aiOfst[nArg] = h;
+ while( z[h] && !IsSpace(z[h]) ){ h++; }
+ if( z[h] ) z[h++] = 0;
}
- return SQLITE_OK;
- }
- if( faultsim_state.eVerbose>=1 ){
- sqlite3_fprintf(stdout,
- "FAULT-SIM id=%d returns %d\n", iArg, faultsim_state.iErr);
- }
- faultsim_state.iCnt = faultsim_state.iInterval;
- faultsim_state.nHit++;
- if( faultsim_state.nRepeat>0 && faultsim_state.nRepeat<=faultsim_state.nHit ){
- faultsim_state.iCnt = -1;
+ nArg++;
}
- return faultsim_state.iErr;
+ p->dot.nArg = nArg;
- if( p->dot.nAlloc==0 ){
- return 0; /* No input tokens */
- }
+ p->dot.azArg[nArg] = 0;
+ }
+
+ /*
+ ** If an input line begins with "." then invoke this routine to
+ ** process that line.
+ **
+ ** Return 1 on error, 2 to exit, and 0 otherwise.
+ */
-static int do_meta_command(char *zLine, ShellState *p){
- int h = 1;
- int nArg = 0;
++static int do_meta_command(const char *zLine, ShellState *p){
++ int nArg;
+ int n, c;
+ int rc = 0;
- char *azArg[52];
++ char **azArg;
+
+ #if !defined(SQLITE_OMIT_VIRTUALTABLE) && !defined(SQLITE_OMIT_AUTHORIZATION)
+ if( p->expert.pExpert ){
+ expertFinish(p, 1, 0);
+ }
+ #endif
+
- /* Parse the input line into tokens.
++ /* Parse the input line into tokens stored in p->dot.
+ */
- while( zLine[h] && nArg<ArraySize(azArg)-1 ){
- while( IsSpace(zLine[h]) ){ h++; }
- if( zLine[h]==0 ) break;
- if( zLine[h]=='\'' || zLine[h]=='"' ){
- int delim = zLine[h++];
- azArg[nArg++] = &zLine[h];
- while( zLine[h] && zLine[h]!=delim ){
- if( zLine[h]=='\\' && delim=='"' && zLine[h+1]!=0 ) h++;
- h++;
- }
- if( zLine[h]==delim ){
- zLine[h++] = 0;
- }
- if( delim=='"' ) resolve_backslashes(azArg[nArg-1]);
- }else{
- azArg[nArg++] = &zLine[h];
- while( zLine[h] && !IsSpace(zLine[h]) ){ h++; }
- if( zLine[h] ) zLine[h++] = 0;
- }
- }
- azArg[nArg] = 0;
++ parseDotCmdArgs(zLine, p);
++ nArg = p->dot.nArg;
+ azArg = p->dot.azArg;
/* Process the input line.
*/
for(i=0; i<argcToFree; i++) free(argvToFree[i]);
free(argvToFree);
#endif
- free(data.colWidth);
+ modeFree(&data.mode);
+ free(data.zErrPrefix);
free(data.zNonce);
- sqlite3_fprintf(stderr, "%d test%s run with %d error%s\n",
+ free(data.dot.zCopy);
+ free(data.dot.azArg);
+ free(data.dot.aiOfst);
+ free(data.dot.abQuot);
+ if( data.nTestRun ){
++ sqlite3_fprintf(stdout, "%d test%s run with %d error%s\n",
+ data.nTestRun, data.nTestRun==1 ? "" : "s",
+ data.nTestErr, data.nTestErr==1 ? "" : "s");
++ fflush(stdout);
+ rc = data.nTestErr>0;
+ }
/* Clear the global data structure so that valgrind will detect memory
** leaks */
memset(&data, 0, sizeof(data));
--- /dev/null
- }
+# 2025-11-05
+#
+# 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.
+#
+#***********************************************************************
+#
+# Test cases for the Query Result Formatter (QRF)
+#
+
+set testdir [file dirname $argv0]
+source $testdir/tester.tcl
+set testprefix qrf01
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a, b, c);
+ INSERT INTO t1 VALUES(1,2.5,'three'),(x'424c4f42',NULL,'Ἀμήν');
+}
+
+do_test 1.10 {
+ set result "\n[db format {SELECT * FROM t1}]"
+} {
+┌──────┬─────┬───────┐
+│ a │ b │ c │
+├──────┼─────┼───────┤
+│ 1 │ 2.5 │ three │
+│ BLOB │ │ Ἀμήν │
+└──────┴─────┴───────┘
+}
+do_test 1.11a {
+ set result "\n[db format -title off {SELECT * FROM t1}]"
+} {
+┌──────┬─────┬───────┐
+│ 1 │ 2.5 │ three │
+│ BLOB │ │ Ἀμήν │
+└──────┴─────┴───────┘
+}
+do_test 1.11b {
+ set result "\n[db format -text sql {SELECT * FROM t1}]"
+} {
+┌─────────────┬─────┬─────────┐
+│ a │ b │ c │
+├─────────────┼─────┼─────────┤
+│ 1 │ 2.5 │ 'three' │
+│ x'424c4f42' │ │ 'Ἀμήν' │
+└─────────────┴─────┴─────────┘
+}
+do_test 1.12 {
+ set result "\n[db format -text csv {SELECT * FROM t1}]"
+} {
+┌────────────────────┬─────┬────────┐
+│ a │ b │ c │
+├────────────────────┼─────┼────────┤
+│ 1 │ 2.5 │ three │
+│ "\102\114\117\102" │ │ "Ἀμήν" │
+└────────────────────┴─────┴────────┘
+}
+do_test 1.13 {
+ set result "\n[db format -text csv -blob hex {SELECT * FROM t1}]"
+} {
+┌──────────┬─────┬────────┐
+│ a │ b │ c │
+├──────────┼─────┼────────┤
+│ 1 │ 2.5 │ three │
+│ 424c4f42 │ │ "Ἀμήν" │
+└──────────┴─────┴────────┘
+}
+do_test 1.14 {
+ catch {db format -text unk -blob hex {SELECT * FROM t1}} res
+ set res
+} {bad -text "unk": must be auto, csv, html, json, plain, sql, or tcl}
+do_test 1.15 {
+ catch {db format -text sql -blob unk {SELECT * FROM t1}} res
+ set res
+} {bad BLOB encoding (-blob) "unk": must be auto, hex, json, tcl, text, or sql}
+do_test 1.16 {
+ catch {db format -text sql -style unk {SELECT * FROM t1}} res
+ set res
+} {bad format style (-style) "unk": must be auto, box, column, count, csv, eqp, explain, html, insert, jobject, json, line, list, markdown, quote, stats, stats-est, stats-vm, or table}
+
+
+do_test 1.20 {
+ set result "\n[db format -style box {SELECT * FROM t1}]"
+} {
+┌──────┬─────┬───────┐
+│ a │ b │ c │
+├──────┼─────┼───────┤
+│ 1 │ 2.5 │ three │
+│ BLOB │ │ Ἀμήν │
+└──────┴─────┴───────┘
+}
+
+do_test 1.30 {
+ set result "\n[db format -style table {SELECT * FROM t1}]"
+} {
++------+-----+-------+
+| a | b | c |
++------+-----+-------+
+| 1 | 2.5 | three |
+| BLOB | | Ἀμήν |
++------+-----+-------+
+}
+do_test 1.31 {
+ set result "\n[db format -style table -title off {SELECT * FROM t1}]"
+} {
++------+-----+-------+
+| 1 | 2.5 | three |
+| BLOB | | Ἀμήν |
++------+-----+-------+
+}
+
+do_test 1.40 {
+ set result "\n[db format -style column {SELECT * FROM t1}]"
+} {
+a b c
+---- --- -----
+1 2.5 three
+BLOB Ἀμήν
+}
+do_test 1.41 {
+ set result "\n[db format -style column -title off {SELECT * FROM t1}]"
+} {
+1 2.5 three
+BLOB Ἀμήν
+}
+
+do_test 1.50 {
+ db format -style count {SELECT * FROM t1}
+} 2
+
+do_test 1.60 {
+ db format -style csv {SELECT * FROM t1}
+} "1,2.5,three\r\n\"\\102\\114\\117\\102\",,\"Ἀμήν\"\r\n"
+do_test 1.61 {
+ db format -style csv -title auto {SELECT * FROM t1}
+} "a,b,c\r\n1,2.5,three\r\n\"\\102\\114\\117\\102\",,\"Ἀμήν\"\r\n"
+do_test 1.62 {
+ db format -style csv -title csv {SELECT a AS 'a x y', b, c FROM t1}
+} "\"a x y\",b,c\r\n1,2.5,three\r\n\"\\102\\114\\117\\102\",,\"Ἀμήν\"\r\n"
+
+do_test 1.70 {
+ set result "\n[db format -style html {SELECT * FROM t1}]"
+} {
+<TR>
+<TD>1
+<TD>2.5
+<TD>three
+</TR>
+<TR>
+<TD>BLOB
+<TD>null
+<TD>Ἀμήν
+</TR>
+}
+do_test 1.71 {
+ set result "\n[db format -style html -title auto {SELECT * FROM t1}]"
+} {
+<TR>
+<TH>a
+<TH>b
+<TH>c
+</TR>
+<TR>
+<TD>1
+<TD>2.5
+<TD>three
+</TR>
+<TR>
+<TD>BLOB
+<TD>null
+<TD>Ἀμήν
+</TR>
+}
+
+do_test 1.80 {
+ set result "\n[db format -style insert {SELECT * FROM t1}]"
+} {
+INSERT INTO tab VALUES(1,2.5,'three');
+INSERT INTO tab VALUES(x'424c4f42',NULL,'Ἀμήν');
+}
+do_test 1.81 {
+ set result "\n[db format -style insert -tablename t1 {SELECT * FROM t1}]"
+} {
+INSERT INTO t1 VALUES(1,2.5,'three');
+INSERT INTO t1 VALUES(x'424c4f42',NULL,'Ἀμήν');
+}
+do_test 1.82 {
+ set result "\n[db format -style insert -tablename t1 -title auto \
+ {SELECT * FROM t1}]"
+} {
+INSERT INTO t1(a,b,c) VALUES(1,2.5,'three');
+INSERT INTO t1(a,b,c) VALUES(x'424c4f42',NULL,'Ἀμήν');
+}
+do_test 1.83 {
+ set result "\n[db format -style insert -tablename drop -title on \
+ {SELECT a AS "a-b", b, c AS "123" FROM t1}]"
+} {
+INSERT INTO "drop"("a-b",b,"123") VALUES(1,2.5,'three');
+INSERT INTO "drop"("a-b",b,"123") VALUES(x'424c4f42',NULL,'Ἀμήν');
+}
+
+do_test 1.90 {
+ set result "\n[db format -style json {SELECT * FROM t1}]"
+} {
+[{"a":1,"b":2.5,"c":"three"},
+{"a":"\u0042\u004c\u004f\u0042","b":null,"c":"Ἀμήν"}]
+}
+do_test 1.91 {
+ set result "\n[db format -style jobject {SELECT * FROM t1}]"
+} {
+{"a":1,"b":2.5,"c":"three"}
+{"a":"\u0042\u004c\u004f\u0042","b":null,"c":"Ἀμήν"}
+}
+do_test 1.92 {
+ set result "\n[db format -style jobject {SELECT *, unistr('abc\u000a123\u000d\u000axyz') AS xyz FROM t1}]"
+} {
+{"a":1,"b":2.5,"c":"three","xyz":"abc\n123\r\nxyz"}
+{"a":"\u0042\u004c\u004f\u0042","b":null,"c":"Ἀμήν","xyz":"abc\n123\r\nxyz"}
+}
+
+do_test 1.100 {
+ set result "\n[db format -style line {SELECT * FROM t1}]"
+} {
+a = 1
+b = 2.5
+c = three
+
+a = BLOB
+b =
+c = Ἀμήν
+}
+do_test 1.101 {
+ set result "\n[db format -style line -null (NULL) {SELECT * FROM t1}]"
+} {
+a = 1
+b = 2.5
+c = three
+
+a = BLOB
+b = (NULL)
+c = Ἀμήν
+}
+do_test 1.102 {
+ set result "\n[db format -style line -null (NULL) \
+ -text sql {SELECT * FROM t1}]"
+} {
+a = 1
+b = 2.5
+c = 'three'
+
+a = x'424c4f42'
+b = (NULL)
+c = 'Ἀμήν'
+}
+
+do_test 1.110 {
+ set result "\n[db format -style list {SELECT * FROM t1}]"
+} {
+1|2.5|three
+BLOB||Ἀμήν
+}
+do_test 1.111 {
+ set result "\n[db format -style list -title on {SELECT * FROM t1}]"
+} {
+a|b|c
+1|2.5|three
+BLOB||Ἀμήν
+}
+do_test 1.112 {
+ set result "\n[db format -style list -title on -text sql -null NULL \
+ -title plain {SELECT * FROM t1}]"
+} {
+a|b|c
+1|2.5|'three'
+x'424c4f42'|NULL|'Ἀμήν'
+}
+do_test 1.118 {
+ set rc [catch {db format -style list -title unk {SELECT * FROM t1}} res]
+ lappend rc $res
+} {1 {bad -title "unk": must be off, on, auto, csv, html, json, plain, sql, or tcl}}
+
+
+do_test 1.120 {
+ set result "\n[db format -style markdown {SELECT * FROM t1}]"
+} {
+| a | b | c |
+|------|-----|-------|
+| 1 | 2.5 | three |
+| BLOB | | Ἀμήν |
+}
+do_test 1.121 {
+ set result "\n[db format -style markdown -title off {SELECT * FROM t1}]"
+} {
+| 1 | 2.5 | three |
+| BLOB | | Ἀμήν |
+}
+
+do_test 1.130 {
+ set result "\n[db format -style quote {SELECT * FROM t1}]"
+} {
+1,2.5,'three'
+x'424c4f42',NULL,'Ἀμήν'
+}
+do_test 1.131 {
+ set result "\n[db format -style quote -title on {SELECT * FROM t1}]"
+} {
+'a','b','c'
+1,2.5,'three'
+x'424c4f42',NULL,'Ἀμήν'
+}
+
+
+do_execsql_test 2.0 {
+ DELETE FROM t1;
+ INSERT INTO t1 VALUES(1,2,'The quick fox jumps over the lazy brown dog.');
+}
+do_test 2.1 {
+ set result "\n[db format -widths {5 -5 19} -wordwrap on \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬─────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼─────────────────────┤
+│ 1 │ 2 │ The quick fox jumps │
+│ │ │ over the lazy brown │
+│ │ │ dog. │
+└───────┴───────┴─────────────────────┘
+}
+do_test 2.2 {
+ set result "\n[db format -widths {5 -5 19} -wordwrap off \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬─────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼─────────────────────┤
+│ 1 │ 2 │ The quick fox jumps │
+│ │ │ over the lazy brown │
+│ │ │ dog. │
+└───────┴───────┴─────────────────────┘
+}
+do_test 2.3 {
+ set result "\n[db format -widths {5 -5 18} -wordwrap on \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼────────────────────┤
+│ 1 │ 2 │ The quick fox │
+│ │ │ jumps over the │
+│ │ │ lazy brown dog. │
+└───────┴───────┴────────────────────┘
+}
+do_test 2.4 {
+ set result "\n[db format -widths {5 -5 -18} -wordwrap on \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼────────────────────┤
+│ 1 │ 2 │ The quick fox │
+│ │ │ jumps over the │
+│ │ │ lazy brown dog. │
+└───────┴───────┴────────────────────┘
+}
+do_test 2.5 {
+ set result "\n[db format -widths {5 -5 19} -wordwrap off \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬─────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼─────────────────────┤
+│ 1 │ 2 │ The quick fox jumps │
+│ │ │ over the lazy brown │
+│ │ │ dog. │
+└───────┴───────┴─────────────────────┘
+}
+do_test 2.6 {
+ set result "\n[db format -widths {5 -5 18} -wordwrap off \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼────────────────────┤
+│ 1 │ 2 │ The quick fox jump │
+│ │ │ s over the lazy br │
+│ │ │ own dog. │
+└───────┴───────┴────────────────────┘
+}
+do_test 2.7 {
+ set result "\n[db format -widths {5 5 18} -wordwrap yes \
+ -align {left center right} -titlealign right \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼────────────────────┤
+│ 1 │ 2 │ The quick fox │
+│ │ │ jumps over the │
+│ │ │ lazy brown dog. │
+└───────┴───────┴────────────────────┘
+}
+do_test 2.8 {
+ set result "\n[db format -widths {5 8 11} -wordwrap yes \
+ -align {auto auto center} -titlealign left \
+ -defaultalign right \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬──────────┬─────────────┐
+│ a │ b │ c │
+├───────┼──────────┼─────────────┤
+│ 1 │ 2 │ The quick │
+│ │ │ fox jumps │
+│ │ │ over the │
+│ │ │ lazy brown │
+│ │ │ dog. │
+└───────┴──────────┴─────────────┘
+}
+do_test 2.9 {
+ catch {db format -align {auto xyz 123} {SELECT * FROM t1}} res
+ set res
+} {bad column alignment (-align) "xyz": must be auto, bottom, c, center, e, left, middle, n, ne, nw, right, s, se, sw, top, or w}
+do_test 2.10 {
+ catch {db format -defaultalign xyz {SELECT * FROM t1}} res
+ set res
+} {bad default alignment (-defaultalign) "xyz": must be auto, bottom, c, center, e, left, middle, n, ne, nw, right, s, se, sw, top, or w}
+do_test 2.11 {
+ catch {db format -titlealign xyz {SELECT * FROM t1}} res
+ set res
+} {bad title alignment (-titlealign) "xyz": must be auto, bottom, c, center, e, left, middle, n, ne, nw, right, s, se, sw, top, or w}
+
+
+do_execsql_test 2.30 {
+ UPDATE t1 SET c='Η γρήγορη αλεπού πηδάει πάνω από το τεμπέλικο καφέ σκυλί';
++ SELECT hex(c) FROM t1;
++} {CE9720CEB3CF81CEAECEB3CEBFCF81CEB720CEB1CEBBCEB5CF80CEBFCF8D20CF80CEB7CEB4CEACCEB5CEB920CF80CEACCEBDCF8920CEB1CF80CF8C20CF84CEBF20CF84CEB5CEBCCF80CEADCEBBCEB9CEBACEBF20CEBACEB1CF86CEAD20CF83CEBACF85CEBBCEAF}
+do_test 2.31 {
+ set result "\n[db format -widths {5 -5 18} -wordwrap on \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼────────────────────┤
+│ 1 │ 2 │ Η γρήγορη αλεπού │
+│ │ │ πηδάει πάνω από το │
+│ │ │ τεμπέλικο καφέ │
+│ │ │ σκυλί │
+└───────┴───────┴────────────────────┘
+}
+do_test 2.32 {
+ set result "\n[db format -widths {5 5 18} -align {left center center} -wordwrap on \
+ {SELECT * FROM t1}]"
+} {
+┌───────┬───────┬────────────────────┐
+│ a │ b │ c │
+├───────┼───────┼────────────────────┤
+│ 1 │ 2 │ Η γρήγορη αλεπού │
+│ │ │ πηδάει πάνω από το │
+│ │ │ τεμπέλικο καφέ │
+│ │ │ σκυλί │
+└───────┴───────┴────────────────────┘
+}
+
+
+do_execsql_test 3.0 {
+ DELETE FROM t1;
+ INSERT INTO t1 VALUES(1,2,unistr('abc\u001b[1;31m123\u001b[0mxyz'));
+}
+do_test 3.1 {
+ set result "\n[db format {SELECT * FROM t1}]"
+} {
+┌───┬───┬────────────────────────┐
+│ a │ b │ c │
+├───┼───┼────────────────────────┤
+│ 1 │ 2 │ abc^[[1;31m123^[[0mxyz │
+└───┴───┴────────────────────────┘
+}
+do_test 3.2 {
+ set result "\n[db format -esc off {SELECT * FROM t1}]"
+ string map [list \033 X] $result
+} {
+┌───┬───┬───────────┐
+│ a │ b │ c │
+├───┼───┼───────────┤
+│ 1 │ 2 │ abcX[1;31m123X[0mxyz │
+└───┴───┴───────────┘
+}
+do_test 3.3 {
+ set result "\n[db format -esc symbol {SELECT * FROM t1}]"
+} {
+┌───┬───┬──────────────────────┐
+│ a │ b │ c │
+├───┼───┼──────────────────────┤
+│ 1 │ 2 │ abc␛[1;31m123␛[0mxyz │
+└───┴───┴──────────────────────┘
+}
+do_test 3.4 {
+ set result "\n[db format -esc ascii {SELECT * FROM t1}]"
+} {
+┌───┬───┬────────────────────────┐
+│ a │ b │ c │
+├───┼───┼────────────────────────┤
+│ 1 │ 2 │ abc^[[1;31m123^[[0mxyz │
+└───┴───┴────────────────────────┘
+}
+do_test 3.5 {
+ catch {db format -esc unk {SELECT * FROM t1}} res
+ set res
+} {bad control character escape (-esc) "unk": must be ascii, auto, off, or symbol}
+
+do_execsql_test 4.0 {
+ DELETE FROM t1;
+ INSERT INTO t1 VALUES(json('{a:5,b:6}'), jsonb('{c:1,d:2}'), 99);
+}
+do_test 4.1 {
+ set result "\n[db format -text sql {SELECT * FROM t1}]"
+} {
+┌─────────────────┬───────────────────────┬────┐
+│ a │ b │ c │
+├─────────────────┼───────────────────────┼────┤
+│ '{"a":5,"b":6}' │ x'8c1763133117641332' │ 99 │
+└─────────────────┴───────────────────────┴────┘
+}
+do_test 4.2 {
+ set result "\n[db format -text sql -textjsonb on {SELECT * FROM t1}]"
+} {
+┌─────────────────┬─────────────────┬────┐
+│ a │ b │ c │
+├─────────────────┼─────────────────┼────┤
+│ '{"a":5,"b":6}' │ '{"c":1,"d":2}' │ 99 │
+└─────────────────┴─────────────────┴────┘
+}
+do_test 4.3 {
+ set result "\n[db format -text plain -textjsonb on -wrap 11 \
+ {SELECT a AS json, b AS jsonb, c AS num FROM t1}]"
+} {
+┌─────────────┬─────────────┬─────┐
+│ json │ jsonb │ num │
+├─────────────┼─────────────┼─────┤
+│ {"a":5,"b": │ {"c":1,"d": │ 99 │
+│ 6} │ 2} │ │
+└─────────────┴─────────────┴─────┘
+}
+
+do_execsql_test 5.0 {
+ DROP TABLE t1;
+ CREATE TABLE t1(name, mtime, value);
+ INSERT INTO t1 VALUES
+ ('entry-one',1708791504,zeroblob(300)),
+ (unistr('one\u000atwo\u000athree'),1333206973,NULL),
+ ('sample-jsonb',1333101221,jsonb('{
+ "alpha":53.11688723,
+ "beta":"qrfWidthPrint(p, p->pOut, -p->u.sLine.mxColWth);",
+ "zeta":[15,null,1333206973,"fd8ffe000104a46494600010101"]}'));
+}
+do_test 5.1 {
+ set sql {SELECT name, mtime, datetime(mtime,'unixepoch') AS time,
+ value FROM t1 ORDER BY mtime}
+ set result "\n[db format -style line -screenwidth 60 -blob sql \
+ -text sql -wordwrap off -linelimit 77 $sql]"
+} {
+ name = 'sample-jsonb'
+mtime = 1333101221
+ time = '2012-03-30 09:53:41'
+value = x'cc7c57616c706861b535332e31313638383732334762657461
+ c73071726657696474685072696e7428702c20702d3e704f7574
+ 2c202d702d3e752e734c696e652e6d78436f6c577468293b477a
+ 657461cb2c23313500a331333333323036393733c71b66643866
+ 6665303030313034613436343934363030303130313031'
+
+ name = unistr('one\u000atwo\u000athree')
+mtime = 1333206973
+ time = '2012-03-31 15:16:13'
+value =
+
+ name = 'entry-one'
+mtime = 1708791504
+ time = '2024-02-24 16:18:24'
+value = x'00000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 000000000000000000000000000000'
+}
+do_test 5.2 {
+ set sql {SELECT name, mtime, datetime(mtime,'unixepoch') AS time,
+ value FROM t1 ORDER BY mtime}
+ set result "\n[db format -style line -screenwidth 60 -blob sql \
+ -text plain -esc off -textjsonb yes \
+ -wordwrap yes -linelimit 3 $sql]"
+} {
+ name = sample-jsonb
+mtime = 1333101221
+ time = 2012-03-30 09:53:41
+value = {"alpha":53.11688723,"beta":"qrfWidthPrint(p,
+ p->pOut, -p->u.sLine.mxColWth);","zeta":[15,null,
+ 1333206973,"fd8ffe000104a46494600010101"]}
+
+ name = one
+ two
+ three
+mtime = 1333206973
+ time = 2012-03-31 15:16:13
+value =
+
+ name = entry-one
+mtime = 1708791504
+ time = 2024-02-24 16:18:24
+value = x'00000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000
+ ...
+}
+do_test 5.3a {
+ set result "\n[db format -style box -widths {0 10 10 14}\
+ -align {left right right center} \
+ -blob sql \
+ -text plain -esc off -textjsonb no \
+ -wordwrap yes -linelimit 2 $sql]"
+} {
+┌──────────────┬────────────┬────────────┬────────────────┐
+│ name │ mtime │ time │ value │
+├──────────────┼────────────┼────────────┼────────────────┤
+│ sample-jsonb │ 1333101221 │ 2012-03-30 │ x'cc7c57616c70 │
+│ │ │ 09:53:41 │ 6861b535332e31 │
+│ │ │ │ ... │
+├──────────────┼────────────┼────────────┼────────────────┤
+│ one │ 1333206973 │ 2012-03-31 │ │
+│ two │ │ 15:16:13 │ │
+│ ... │ │ │ │
+├──────────────┼────────────┼────────────┼────────────────┤
+│ entry-one │ 1708791504 │ 2024-02-24 │ x'000000000000 │
+│ │ │ 16:18:24 │ 00000000000000 │
+│ │ │ │ ... │
+└──────────────┴────────────┴────────────┴────────────────┘
+}
+do_test 5.3b {
+ set result "\n[db format -style table -widths {0 10 10 14}\
+ -align {center right right right} \
+ -blob sql \
+ -text plain -esc off -textjsonb no \
+ -wordwrap yes -linelimit 2 $sql]"
+} {
++--------------+------------+------------+----------------+
+| name | mtime | time | value |
++--------------+------------+------------+----------------+
+| sample-jsonb | 1333101221 | 2012-03-30 | x'cc7c57616c70 |
+| | | 09:53:41 | 6861b535332e31 |
+| | | | ... |
++--------------+------------+------------+----------------+
+| one | 1333206973 | 2012-03-31 | |
+| two | | 15:16:13 | |
+| ... | | | |
++--------------+------------+------------+----------------+
+| entry-one | 1708791504 | 2024-02-24 | x'000000000000 |
+| | | 16:18:24 | 00000000000000 |
+| | | | ... |
++--------------+------------+------------+----------------+
+}
+do_test 5.3c {
+ set result "\n[db format -style column -widths {0 10 10 14}\
+ -align {center right right right} \
+ -blob sql \
+ -text plain -esc off -textjsonb no \
+ -wordwrap yes -linelimit 2 $sql]"
+} {
+ name mtime time value
+------------ ---------- ---------- --------------
+sample-jsonb 1333101221 2012-03-30 x'cc7c57616c70
+ 09:53:41 6861b535332e31
+ ...
+
+ one 1333206973 2012-03-31
+ two 15:16:13
+ ...
+
+ entry-one 1708791504 2024-02-24 x'000000000000
+ 16:18:24 00000000000000
+ ...
+}
+do_test 5.4 {
+ db eval {
+ CREATE TABLE t2(a,b,c,d,e);
+ WITH v(x) AS (SELECT 'abcdefghijklmnopqrstuvwxyz')
+ INSERT INTO t2 SELECT x,x,x,x,x FROM v;
+ }
+ set sql {SELECT char(0x61,0xa,0x62,0xa,0x63,0xa,0x64) a,
+ mtime b, mtime c, mtime d, mtime e FROM t1}
+ set result "\n[db format -style box -widths {1 2 3 4 5}\
+ -linelimit 3 -wordwrap off {SELECT *, 'x' AS x FROM t2}]"
+} {
+┌────┬────┬─────┬──────┬───────┬───┐
+│ a │ b │ c │ d │ e │ x │
+├────┼────┼─────┼──────┼───────┼───┤
+│ ab │ ab │ abc │ abcd │ abcde │ x │
+│ cd │ cd │ def │ efgh │ fghij │ │
+│ ef │ ef │ ghi │ ijkl │ klmno │ │
+│ .. │ .. │ ... │ ... │ ... │ │
+└────┴────┴─────┴──────┴───────┴───┘
+}
+
+do_execsql_test 6.0 {
+ DELETE FROM t2;
+ INSERT INTO t2 VALUES
+ (1, 2.5, 'three', x'342028666f757229', null);
+}
+do_test 6.1a {
+ set result "\n[db format -style list -null NULL \
+ -text tcl -columnsep , \
+ {SELECT * FROM t2}]"
+} {
+1,2.5,"three","\064\040\050\146\157\165\162\051",NULL
+}
+do_test 6.1b {
+ set result "\n[db format -style list -null NULL \
+ -text tcl -columnsep , -textnull off \
+ {SELECT * FROM t2}]"
+} {
+1,2.5,"three","\064\040\050\146\157\165\162\051",NULL
+}
+do_test 6.1c {
+ set result "\n[db format -style list -null NULL \
+ -text tcl -columnsep , -textnull auto \
+ {SELECT * FROM t2}]"
+} {
+1,2.5,"three","\064\040\050\146\157\165\162\051",NULL
+}
+do_test 6.2 {
+ set result "\n[db format -style list -null NULL \
+ -text tcl -columnsep , -textnull yes \
+ {SELECT * FROM t2}]"
+} {
+1,2.5,"three","\064\040\050\146\157\165\162\051","NULL"
+}
+do_test 6.3 {
+ catch {db format -textnull xyz {SELECT * FROM t2}} res
+ set res
+} {bad -textnull "xyz": must be auto, yes, no, on, or off}
+
+finish_test