From: drh <> Date: Fri, 14 Nov 2025 13:07:45 +0000 (+0000) Subject: Fix various bugs and compiler warnings. All tests now passing on linux, mac, X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d5ee8aa625df7f91519495ddacd9143196c5750d;p=thirdparty%2Fsqlite.git Fix various bugs and compiler warnings. All tests now passing on linux, mac, and windows. More testing needed, though. FossilOrigin-Name: 2220cb70c2f1ee30dcdf917a20feacdfcb3789433d0645fea626fd4c5cf0d099 --- d5ee8aa625df7f91519495ddacd9143196c5750d diff --cc ext/qrf/qrf.c index 8338061ea7,0000000000..2646b8689e mode 100644,000000..100644 --- a/ext/qrf/qrf.c +++ b/ext/qrf/qrf.c @@@ -1,2235 -1,0 +1,2266 @@@ +/* +** 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 - #include +#include + +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 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( z0 ){ + 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>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>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; in; 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=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( bWrap && z[i]!=0 && !isspace(z[i]) && isalnum(c)==isalnum(z[i]) ){ ++ 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( isspace(z[k]) ) break; ++ if( qrfSpace(z[k]) ) break; + } + if( k=i/2; k--){ - if( isalnum(z[k-1])!=isalnum(z[k]) && (z[k]&0xc0)!=0x80 ) break; ++ 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; inCol; 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; inCol; 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; inCol; i++){ + if( ispec.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( ispec.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; ipStmt,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; idb); + 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; ispec.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 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 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; ipOut, rowStart, szRowStart); + bMore = 0; + for(j=0; jpOut, data.azThis[j], nThis, nWS, data.aAlign[j]); + data.azThis[j] += iNext; + if( data.azThis[j][0]!=0 ) bMore = 1; + if( jpOut, 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; jpOut, 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( jpOut, 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+nColumnspec.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; jpOut, data.aiCol[j], '-'); + if( jpOut, 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; ipStmt); + 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; ipStmt, 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; ipOut, "%.*c", aWidth[i], '-'); + if( i==nArg-1 ){ + sqlite3_str_append(p->pOut, "\n", 1); + }else{ + sqlite3_str_append(p->pOut, " ", 2); + } + } + } + + for(i=0; ipStmt, 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 && iOppOut, 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( !isalpha(z[0]) && z[0]!='_' ) return 1; ++ if( !qrfAlpha(z[0]) ) return 1; + for(i=0; z[i]; i++){ - if( !isalnum(z[i]) && z[i]!='_' ) return 1; ++ 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; inCol; 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, "", 4); + for(i=0; inCol; i++){ + const char *zCName = sqlite3_column_name(p->pStmt, i); + sqlite3_str_append(p->pOut, "\n", 5); + qrfEncodeText(p, p->pOut, zCName); + } + sqlite3_str_append(p->pOut, "\n\n", 7); + } + sqlite3_str_append(p->pOut, "", 4); + for(i=0; inCol; i++){ + sqlite3_str_append(p->pOut, "\n", 5); + qrfRenderValue(p, p->pOut, i); + } + sqlite3_str_append(p->pOut, "\n\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; inCol; 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; inCol; 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; inCol; 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; inCol; 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; inCol; 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; inCol; 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; +} diff --cc manifest index 09e9c8cae3,9b9b877350..0f5d9d7410 --- a/manifest +++ b/manifest @@@ -1,5 -1,5 +1,5 @@@ - C Merge\scompiler-warning\sfix\sfrom\strunk\sinto\sthe\sqrf\sbranch. - D 2025-11-14T11:06:27.128 -C Fix\sa\sharmless\scompiler\swarning\sin\stesting\scode. -D 2025-11-14T11:02:49.203 ++C Fix\svarious\sbugs\sand\scompiler\swarnings.\s\sAll\stests\snow\spassing\son\slinux,\smac,\nand\swindows.\s\sMore\stesting\sneeded,\sthough. ++D 2025-11-14T13:07:45.192 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@@ -416,9 -416,6 +416,9 @@@ F ext/misc/wholenumber.c 0fa0c082676b78 F ext/misc/windirent.h 02211ce51f3034c675f2dbf4d228194d51b3ee05734678bad5106fff6292e60c F ext/misc/zipfile.c 09e6e3a3ff40a99677de3c0bc6569bd5f4709b1844ac3d1c1452a456c5a62f1c F ext/misc/zorder.c bddff2e1b9661a90c95c2a9a9c7ecd8908afab5763256294dd12d609d4664eee +F ext/qrf/README.md db3710552dfdacfc600e4c9772f84dccdf84ceb9983837a86269d4f6cb67219a - F ext/qrf/qrf.c 259d4391d68c865f11eb36f65c8c89da7d9aa0ff2de9a507d9470cb13b7c6e7f ++F ext/qrf/qrf.c 7175ce8274b1566672cf26e29be06b39547cf6761bf75bdda4d8355f6b5837a9 +F ext/qrf/qrf.h b4b3489b3b3683523fd248d15cf5945830643b036943efacdb772a3e00367aa2 F ext/rbu/rbu.c 801450b24eaf14440d8fd20385aacc751d5c9d6123398df41b1b5aa804bf4ce8 F ext/rbu/rbu1.test 25870dd7db7eb5597e2b4d6e29e7a7e095abf332660f67d89959552ce8f8f255 F ext/rbu/rbu10.test 7c22caa32c2ff26983ca8320779a31495a6555737684af7aba3daaf762ef3363 @@@ -735,7 -732,7 +735,7 @@@ F src/random.c 606b00941a1d7dd09c381d32 F src/resolve.c 5616fbcf3b833c7c705b24371828215ad0925d0c0073216c4f153348d5753f0a F src/rowset.c 8432130e6c344b3401a8874c3cb49fefe6873fec593294de077afea2dce5ec97 F src/select.c ba9cd07ffa3277883c1986085f6ddc4320f4d35d5f212ab58df79a7ecc1a576a - F src/shell.c.in cecf781e9b57d7adbc65bfb76cfcea4351bb5ad859fd822db77ebdc3f75847d6 -F src/shell.c.in ceb0a9cc008ac82d8d2e6ef353db14a54bc40dfd60a8cfbb6bc98d071f538761 ++F src/shell.c.in 4f034dea340795f025160a81b0ea26d27729376df23b0a9bc784cc328e8aefd2 F src/sqlite.h.in 684c19c3b093cca7a38e3f6405d067777464285f17a58a78f7f89d6763e011e7 F src/sqlite3.rc 015537e6ac1eec6c7050e17b616c2ffe6f70fca241835a84a4f0d5937383c479 F src/sqlite3ext.h 7f236ca1b175ffe03316d974ef57df79b3938466c28d2f95caef5e08c57f3a52 @@@ -1507,8 -1503,6 +1507,8 @@@ F test/printf2.test 3f55c1871a5a6550741 F test/progress.test ebab27f670bd0d4eb9d20d49cef96e68141d92fb F test/ptrchng.test ef1aa72d6cf35a2bbd0869a649b744e9d84977fc F test/pushdown.test 46a626ef1c0ca79b85296ff2e078b9da20a50e9b804b38f441590c3987580ddd - F test/qrf01.test 8e10a9d5f69db76666191899d0f3e48bf1e1ea55d81905ebb1a3e59aaf9388fc ++F test/qrf01.test 3e863267cf237516a5803d94e19046005cf148623ea24c8646acbd304a598c78 +F test/qrf02.test 39b4afdc000bedccdafc0aecf17638df67a67aaa2d2942865ae6abcc48ba0e92 F test/queryonly.test 5f653159e0f552f0552d43259890c1089391dcca F test/quick.test 1681febc928d686362d50057c642f77a02c62e57 F test/quickcheck.test a4b7e878cd97e46108291c409b0bf8214f29e18fddd68a42bc5c1375ad1fb80a @@@ -1602,17 -1596,16 +1602,17 @@@ F test/sharedA.test 64bdd21216dda2c6a3b F test/sharedB.test 1a84863d7a2204e0d42f2e1606577c5e92e4473fa37ea0f5bdf829e4bf8ee707 F test/shared_err.test 32634e404a3317eeb94abc7a099c556a346fdb8fb3858dbe222a4cbb8926a939 F test/sharedlock.test 5ede3c37439067c43b0198f580fd374ebf15d304 - F test/shell1.test f16539b28a1dd53eb88733b6010350fd861d073adada16a4158d8a3e235e9d0d -F test/shell1.test ebe953d64c937ad42a0f33170ac0d2d2568faae26813fc7a95203756446d54aa -F test/shell2.test ab23f01ea2347e4b72bb2399af7ee82aa00f9c059141749f7c4064abca5ad728 ++F test/shell1.test 6b256fffeac83d62e3625f47e7ef4a25db73215349bb92e818c9692ac403da3d +F test/shell2.test d8da6a06dcce1d8f04f776f918d4d57c28ddc28c54f3a44f95429794892e3a91 F test/shell3.test 603b448e917537cf77be0f265c05c6f63bc677c63a533c8e96aae923b56f4a0e F test/shell4.test 03593fa7908a55f255916ffeda707cdf55680c777736e3da62b1d78cde0d684d -F test/shell5.test d17e7927ab8b7f720efbdd9b5d05fceb6c3c56c25917901b315400214bf24ef4 +F test/shell5.test 683f9b5df61192426d030874a04adcb15b5f14c5f3062e2637d4378a3e7224f8 F test/shell6.test e3b883b61d4916b6906678a35f9d19054861123ad91b856461e0a456273bdbb8 F test/shell7.test 43fd8e511c533bab5232e95c7b4be93b243451709e89582600d4b6e67693d5c3 F test/shell8.test 641cf21a99c59404c24e3062923734951c4099a6b6b6520de00cf7a1249ee871 F test/shell9.test 8742a5b390cdcef6369f5aa223e415aa4255a4129ef249b177887dc635a87209 -F test/shellA.test 4ecff8b7b2c0122ba8174abfbcc4b0f59e44d80f2a911068f8cd4cfc6661032d +F test/shellA.test 3a3ccbac90367d51f12c3d762c90e84723492e3bc5d228cfefd4c6ebfc72394d - F test/shellB.test 714c13ba4d964277ab236fa210d8c8e05b5f224ce342981548ef4929794d3768 ++F test/shellB.test ca8a5ce5b9a59098732fa140c911162f0306f70239c6c2de6da9b718ca304cad F test/shmlock.test 9f1f729a7fe2c46c88b156af819ac9b72c0714ac6f7246638a73c5752b5fd13c F test/shortread1.test bb591ef20f0fd9ed26d0d12e80eee6d7ac8897a3 F test/show_speedtest1_rtree.tcl 32e6c5f073d7426148a6936a0408f4b5b169aba5 @@@ -2175,8 -2167,8 +2175,8 @@@ F tool/version-info.c 33d0390ef484b3b1c F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7 F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f - P 3300ed34b5a3598c46cdc4bdf1e9e81818a5029585ae917424f64c11c718bfa8 5252a2e629e1adb61169d32ca6458c6decd1ec562f358bb9d0b448a2f0243c56 - R f0481fcb31b39097a1040d0ea2b4dd6d -P bf399992cb98e5d5f002a90b521328d5c2f113ebab8601653452d78222077bde -R 822e867bdd04a0341d8323d597259571 ++P 6ffab43ca32230975e79d91080dfa2e80a4c21deef31ab86455581af18a399cd 5252a2e629e1adb61169d32ca6458c6decd1ec562f358bb9d0b448a2f0243c56 ++R 55001368278a1a9bd00f0d0045e947e0 U drh - Z b51caee9fb8648c2e805a8f6306d9bd9 -Z 2d7ed663e8bf5d6719badf286180c421 ++Z 4eb8cd588f9e324fca2d92f8eebd5607 # Remove this line to create a well-formed Fossil manifest. diff --cc manifest.uuid index ebcdd9899b,04059e98a2..6e66bf68bc --- a/manifest.uuid +++ b/manifest.uuid @@@ -1,1 -1,1 +1,1 @@@ - 6ffab43ca32230975e79d91080dfa2e80a4c21deef31ab86455581af18a399cd -5252a2e629e1adb61169d32ca6458c6decd1ec562f358bb9d0b448a2f0243c56 ++2220cb70c2f1ee30dcdf917a20feacdfcb3789433d0645fea626fd4c5cf0d099 diff --cc src/shell.c.in index 3bb057fc98,85ec8f3c03..49710dc6de --- a/src/shell.c.in +++ b/src/shell.c.in @@@ -718,10 -918,30 +718,10 @@@ static void SQLITE_CDECL iotracePrintf( ** 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; } /* @@@ -1677,45 -1752,10 +1677,45 @@@ static void failIfSafeMode 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 && iArgdot.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) @@@ -7072,1030 -8657,150 +7072,1036 @@@ FROM ( 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=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+1mode.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; kmode.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+10 || 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; imode.spec.nAlign; i++){ - unsigned char a = p->mode.spec.aAlign[i]; ++ for(ii=0; iimode.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; imode.spec.nWidth; i++){ - sqlite3_str_appendf(pDesc, "%s%d", zSep, (int)p->mode.spec.aWidth[i]); ++ for(ii=0; iimode.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 ){ + 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+1outCount = 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( + "\n\n\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. */ @@@ -12931,19 -13942,8 +12937,20 @@@ int SQLITE_CDECL wmain(int argc, wchar_ for(i=0; i<argcToFree; i++) free(argvToFree[i]); free(argvToFree); #endif - free(data.colWidth); + modeFree(&data.mode); + free(data.zErrPrefix); free(data.zNonce); + free(data.dot.zCopy); + free(data.dot.azArg); + free(data.dot.aiOfst); + free(data.dot.abQuot); + if( data.nTestRun ){ - sqlite3_fprintf(stderr, "%d test%s run with %d error%s\n", ++ 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)); diff --cc test/qrf01.test index 1fb8629b13,0000000000..f13c5d1e3c mode 100644,000000..100644 --- a/test/qrf01.test +++ b/test/qrf01.test @@@ -1,749 -1,0 +1,750 @@@ +# 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 diff --cc test/shell1.test index 1203bcb028,abf214a907..58b250cae7 --- a/test/shell1.test +++ b/test/shell1.test @@@ -1334,12 -1344,12 +1334,12 @@@ select * from t; # do_test shell1-9.1 { catchcmd :memory: { --.mode csv ++.mode csv --rowsep "\n" /* x */ select 1,2; --x -- .nada ; --.mode csv ++.mode csv --rowsep "\n" --x select 2,1; select 3,4; } diff --cc test/shellB.test index ed0a20cef4,0000000000..4802ed1a54 mode 100644,000000..100644 --- a/test/shellB.test +++ b/test/shellB.test @@@ -1,43 -1,0 +1,44 @@@ +# 2025-11-12 +# +# 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. +# +#*********************************************************************** +# TESTRUNNER: shell +# +# Test cases for the command-line shell using the new ".output memory" +# feature. +# +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set CLI [test_cli_invocation] + +# Run an instance of the CLI on the file $name. +# Capture the number of test cases and the number of +# errors and increment the counts. +# +proc do_clitest {name} { - set mapping [list <NAME> $name] ++ set mapping [list <NAME> $::testdir/$name <CLI> $::CLI] + set script [string map $mapping { - catch {exec {*}$::CLI :memory: ".read $::testdir/<NAME>" 2>@stdout} res ++ catch {exec <CLI> :memory: ".read <NAME>" 2>@stdout} res + set ntest 0 + set nerr 999 + regexp {.*(\d+) tests? run with (\d+) errors?} $res all ntest nerr + set_test_counter count [expr {[set_test_counter count]+$ntest-1}] + set_test_counter errors [expr {[set_test_counter errors]+$nerr}] - set answer "error count: $nerr" ++ if {$nerr==0} {set res "error count: 0"} ++ set res + }] + # puts $script + do_test shellB-$name $script {error count: 0} +} + +do_clitest modeA.clitest + +finish_test