From: larrybr Date: Fri, 29 Apr 2022 18:36:17 +0000 (+0000) Subject: Get MSVC build going for sqlite3x.exe and extensions. X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=6aae48ecedfa516591df65bdf92bca6730eaa9e7;p=thirdparty%2Fsqlite.git Get MSVC build going for sqlite3x.exe and extensions. FossilOrigin-Name: cd5e57e0fe4ef2a59c1202b66f4e774f6642fa826cfe3840a786104841dde2f6 --- 6aae48ecedfa516591df65bdf92bca6730eaa9e7 diff --cc Makefile.msc index 8215677a21,d67b7f0b86..32c758ae27 --- a/Makefile.msc +++ b/Makefile.msc @@@ -1671,6 -1670,6 +1671,11 @@@ TESTPROGS = dbhash.exe \ sqltclsh.exe ++TESTPROGS_SHX = \ ++ testfixture.exe \ ++ $(SQLITE3XEXE) \ ++ shell_extensions ++ # Databases containing fuzzer test cases # FUZZDATA = \ @@@ -2385,6 -2377,6 +2390,36 @@@ fts5.dll: fts5_ext.l sqlite3rbu.lo: $(TOP)\ext\rbu\sqlite3rbu.c $(HDR) $(EXTHDR) $(LTCOMPILE) -DSQLITE_CORE -c $(TOP)\ext\rbu\sqlite3rbu.c ++# Rule to build shx_link.h (for use by shell extensions) ++ ++SHX_LINK_SRC = \ ++ $(TOP)/src/shext_linkage.h \ ++ $(TOP)/src/obj_interfaces.h \ ++ sqlite3ext.h \ ++ sqlite3.h ++ ++shx_link.h: $(SHX_LINK_SRC) ++ $(TCLSH_CMD) $(TOP)\tool\mkshellc.tcl -short-head -header-gen \ ++ $(TOP)/src/shext_linkage.h > $@ ++ ++# Rules to build shell extensions used for testing extensible shell ++ ++shell_extensions: tcl_shell_extension \ ++ test_shellext_cpp.dll test_shellext_c.dll ++ ++test_shellext_cpp.dll: $(TOP)/src/test_shellext_cpp.cpp $(TOP)/shx_link.h ++ $(TCC) $(TOP)/src/test_shellext_cpp.cpp -LD -Fe$@ ++ ++test_shellext_c.dll: $(TOP)/src/test_shellext_c.c $(TOP)/shx_link.h ++ $(TCC) $(TOP)/src/test_shellext_c.c -LD -Fe$@ ++ ++tclshext.c: $(TOP)/ext/misc/tclshext.c.in $(TOP)/src/tclsqlite.c ++ $(TCLSH_CMD) $(TOP)\tool\mkshellc.tcl $(TOP)/ext/misc/tclshext.c.in > $@ ++ ++tclshext.dll: tclshext.c shx_link.h ++ $(TCC) -I$(TCLINCDIR) tclshext.c -Fe$@ -link -DLL \ ++ -LIBPATH:$(TCLLIBDIR) $(TCLLIBS) ++ # Rules to build the 'testfixture' application. # # If using the amalgamation, use sqlite3.c directly to build the test diff --cc ext/misc/tclshext.c.in index 227ed247e1,0000000000..86293d6ad1 mode 100644,000000..100644 --- a/ext/misc/tclshext.c.in +++ b/ext/misc/tclshext.c.in @@@ -1,1305 -1,0 +1,1305 @@@ +/* +** 2022 March 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. +** +************************************************************************* +** This file contains code to implement the "tclshext" shell extension +** for use with the extensible "sqlite3" CLI shell. On *Nix, build thusly: + tool/mkshellc.tcl ext/misc/tclshext.c.in > tclshext.c + gcc -shared -fPIC -O2 -I. -Isrc -I/usr/include/tcl8.6 \ + tclshext.c -o tclshext.so -ltcl8.6 +** Or, (after ./configure ...), use the provided Makefile thusly: + make tcl_shell_extension +** If the Tk library is available, it can be linked and used thusly: + gcc -shared -fPIC -O2 -I. -Isrc -I/usr/include/tcl8.6 \ + -DSHELL_ENABLE_TK tclshext.c -o tclshext.so -ltcl8.6 -ltk8.6 +** Or, make the same target with Tk thusly: + make tcl_shell_extension WITH_TK=1 +** Later TCL versions can be used if desired (and installed.) +** +** TCL scripting support is added with a registerScripting() call (in the +** ShellExtensionAPI), meeting ScriptingSupport interface requirements. +*/ +static const char * const zTclHelp = + "This extension adds these features to the host shell:\n" + " 1. TCL scripting support is added.\n" + " 2. TCL commands are added: udb shdb now_interactive get_tcl_group ..\n" + " 3. The .tcl and .unknown dot commands are added.\n" + " 4. If built with Tk capability, the gui TCL command will be added if this\n" + " extension was loaded using shell command: .load tclshext -shext -tk .\n" + " Any other arguments beyond -shext are copied into TCL's argv variable.\n" + "Operation:\n" + " Shell input groups beginning with \"..\" are treated as TCL input, in\n" + " these ways: (1) When a bare \"..\" is entered, a TCL REPL loop is run\n" + " until the end of input is seen; (2) When \"..D ...\" is entered, (where\n" + " \"D\" is a dot-command name), the D dot command will be run in its normal\n" + " fashion, but its arguments will be collected according to TCL parsing\n" + " rules then expanded as usual for TCL commands; and (3) when \".. T ...\"\n" + " is entered, (where \"T\" is a TCL command name), that TCL command and its\n" + " arguments will be collected and expanded according to TCL parsing rules,\n" + " then run in the TCL execution environment (in its global namespace), but\n" + " the shell REPL and execution environment remains in effect afterward.\n" + "\n" + " Note that cases 2 and 3 differ in having space after the leading \"..\".\n" + "\n" + " The phrase \"end of input\" means either: end-of-file is seen on a file,\n" + " pipe or string stream input, or a lone \".\" on the first and only line\n" + " of an input line group is seen. This convention is useful in scripting\n" + " when it is expedient to switch execution environments from within the\n" + " same input stream. This could be input piped in from another process.\n" + "\n" + ; +/* +** For example: + # From shell, enter TCL REPL: + .. + # Initialize some variables and insert into the DB: + set var1 [compute_however ...] + set var2 [derive_somehow ...] + udb eval { INSERT INTO SomeTable(col1, col2) VALUES($var1, var2) } + # Leave REPL + . + # Generate and keep pretty output: + .mode box -ww + .header on + .once prettified.txt + SELECT * FROM SomeTable; + # Alternatively, the query can be run from the TCL environment: + .. + set tstamp [clock format [clock seconds] -format %Y-%m-%d] + .once "backup of prettified.txt made $tstamp" + .eval {SELECT col1, col2 FROM SomeTable} + # Return to shell environment: + . +** +** For any of these ways of providing TCL input, the same TCL interpreter +** is used, with its state maintained from one input to the next. In this +** way, .sqliterc or other preparatory shell scripts (or typing) can be +** made to provide useful, user-defined shell enhancements or specialized +** procedures (aka "TCL commands") for oft-repeated tasks. +** +** The added TCL commands are: +** udb shdb ; # exposes the user DB and shell DB for access via TCL +** now_interactive ; # indicates whether current input is interactive +** get_tcl_group ; # gets one TCL input line group from current input +** register_adhoc_command ; # aids creation of dot commands with help +** .. ; # does nothing, silently and without error +** +** The .. command exists so that a lone ".." on an input line suffices +** to ensure the TCL REPL is running. This is symmetric with a lone "." +** input to the TCL REPL because it either terminates the loop or, if +** entered in the shell environment, quietly does nothing without error. +** +** The added .tcl dot command may be used to enter a TCL REPL, or with +** arguments, it will read files as TCL. (This is somewhat extraneous, +** as the same can be done with TCL commands, but it is more easily done +** from the shell invocation, and the .tcl command's integration into +** the .help facility provides a way for users to get help for "..".) +** +** The added .unknown dot command overrides the shell's .unknown so +** that new dot commands can be implemented in TCL and then be run +** from the shell in the dot command execution context. +*/ + +#include "shx_link.h" + +/* Extension boiler-plate to dynamically link into host's SQLite library */ +SQLITE_EXTENSION_INIT1; + +/* Extension boiler-plate for a function to get ShellExtensionLink pointer + * from db passed to extension init() and define a pair of static API refs. + */ +SHELL_EXTENSION_INIT1(pShExtApi, pExtHelpers, shextLinkFetcher); +#define SHX_API(entry) pShExtApi->entry +#define SHX_HELPER(entry) pExtHelpers->entry + +/* This is not found in the API pointer table published for extensions: */ +#define sqlite3_enable_load_extension SHX_HELPER(enable_load_extension) + +/* Forward reference for use as ExtensionId */ +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_tclshext_init(sqlite3*, char**, const sqlite3_api_routines*); + +/* Control how tclsqlite.c compiles. (REPL is effected here, not there.) */ +#define STATIC_BUILD /* Not publishing TCL API */ +#undef SQLITE_AMALGAMATION +#undef TCLSH +#include "tclOO.h" +#ifdef SHELL_ENABLE_TK +#include "tk.h" /* Only used if option -tk passed during load. */ +#endif +INCLUDE tclsqlite.c + +#if defined(_WIN32) || defined(WIN32) - # define getDir(cArray) _getwd(cArray) ++# include ++# define getDir(cArray) _getcwd(cArray, sizeof(cArray)) +# define chdir(s) _chdir(s) +#else +# define getDir(cArray) getcwd(cArray, sizeof(cArray)) +#endif + +typedef struct TclCmd TclCmd; +typedef struct UnkCmd UnkCmd; + +static struct InterpManage { + Tcl_Interp *pInterp; + int nRefs; +} interpKeep = { 0, 0 }; + +static Tcl_Interp *getInterp(){ + assert(interpKeep.nRefs>0 && interpKeep.pInterp!=0); + return interpKeep.pInterp; +} + +static void Tcl_TakeDown(void *pv){ + assert(pv==&interpKeep); + if( --interpKeep.nRefs==0 ){ + if( interpKeep.pInterp ){ + Tcl_DeleteInterp(interpKeep.pInterp); + Tcl_Release(interpKeep.pInterp); + interpKeep.pInterp = 0; + Tcl_Finalize(); + } + } +} + +static int Tcl_BringUp( +#ifdef SHELL_ENABLE_TK + int *pWithTk, +#endif + char **pzErrMsg){ + if( ++interpKeep.nRefs==1 ){ + const char *zShellName = SHX_HELPER(shellInvokedAs)(); + const char *zShellDir = SHX_HELPER(shellStartupDir)(); + if( zShellDir!=0 ){ + char cwd[FILENAME_MAX+1]; + if( getDir(cwd) && 0==chdir(zShellDir) ){ + int rc; + Tcl_FindExecutable(zShellName); + rc = chdir(cwd); /* result ignored, kept only to silence gcc */ + } + } + interpKeep.pInterp = Tcl_CreateInterp(); + Tcl_SetSystemEncoding(interpKeep.pInterp, "utf-8"); + Sqlite3_Init(interpKeep.pInterp); + Tcl_Preserve(interpKeep.pInterp); + if( 0==Tcl_OOInitStubs(interpKeep.pInterp) ){ + *pzErrMsg = sqlite3_mprintf("Tcl v8.6 or higher required.\n"); + Tcl_TakeDown(&interpKeep); + return SQLITE_ERROR; + } + if( Tcl_Init(interpKeep.pInterp)!=TCL_OK ){ + *pzErrMsg = sqlite3_mprintf("Tcl interpreter startup failed.\n"); + Tcl_TakeDown(&interpKeep); + return SQLITE_ERROR; + } +#ifdef SHELL_ENABLE_TK + else if( *pWithTk ){ + if( TCL_OK!=Tk_Init(interpKeep.pInterp) ){ + fprintf(stderr, "Could not load/initialize Tk." + " (Non-fatal, extension is loaded.)\n"); + *pWithTk = 0; + } + } +#endif + } + return (interpKeep.pInterp!=0)? SQLITE_OK : SQLITE_ERROR; +} + +static void copy_complaint(char **pzErr, Tcl_Interp *pi); +static DotCmdRC runTclREPL(Tcl_Interp *interp, char **pzErrMsg); + +/* Following DERIVED_METHOD(...) macro calls' arguments were copied and + * pasted from the respective interface declarations in shext_linkage.h + */ + +/* This is in the interface for anouncing what was just provided. */ +DERIVED_METHOD(const char *, name, ScriptSupport,TclSS, 0,()){ + (void)(pThis); + return "TclTk"; +} + +/* Provide help for users of this scripting implementation. */ +DERIVED_METHOD(const char *, help, ScriptSupport,TclSS, 1,(const char *zHK)){ + (void)(pThis); + if( zHK==0 ){ + return "Provides TCL scripting support for SQLite extensible shell.\n"; + }else if( *zHK==0 ) return zTclHelp; + return 0; +} + +/* Not doing this yet. */ +DERIVED_METHOD(int, configure, ScriptSupport,TclSS, + 4,( ShellExState *pSES, char **pzErr, + int numArgs, char *azArgs[] )){ + (void)(pThis); + return 0; +} + +/* Say line is script lead-in iff its first dark is "..". + * In combination with dot commands also being TCL commands and the + * special handling in the next three functions, this effects what is + * promised in this file's header text and by .tcl's help text. + */ +DERIVED_METHOD(int, isScriptLeader, ScriptSupport,TclSS, + 1,( const char *zScriptLeadingLine )){ + char c; + (void)(pThis); + while( (c=*zScriptLeadingLine++) && (c==' '||c=='\t') ) {} + return (c=='.' && *zScriptLeadingLine=='.'); +} + +/* Say line group is complete if it passes muster as ready-to-go TCL. */ +DERIVED_METHOD(int, scriptIsComplete, ScriptSupport,TclSS, + 2,( const char *zScript, char **pzWhyNot )){ + (void)(pThis); + (void)(pzWhyNot); + return Tcl_CommandComplete(zScript); +} + +/* As we rely on Tcl_CommandComplete(), no resumable scanning is done. */ +DERIVED_METHOD(void, resetCompletionScan, ScriptSupport,TclSS, 0,()){ + (void)(pThis); +} + +/* Run as TCL after some jiggering with the leading dots. */ +DERIVED_METHOD(DotCmdRC, runScript, ScriptSupport,TclSS, + 3,( const char *zScript, ShellExState *psx, char **pzErrMsg )){ + char c; + Tcl_Interp *interp = getInterp(); + (void)(pThis); + (void)(psx); + + if( interp==0 ) return DCR_Error; + while( (c=*zScript++) && (c==' '||c=='\t') ) {} + if( c=='.' && *zScript++=='.' ){ - int rc, nc = strlen30(zScript); ++ int rc = 0; ++ int nc = strlen30(zScript); + /* At this point, *zScript should fall into one of these cases: */ + switch( *zScript ){ + case '.': + /* Three dots, assume user meant to run a dot command. */ + one_shot_tcl: + rc = Tcl_EvalEx(interp, zScript, /* needs no adjustment */ + nc, TCL_EVAL_DIRECT|TCL_EVAL_GLOBAL); + if( rc!=TCL_OK ) copy_complaint(pzErrMsg, getInterp()); + break; + case ' ': case '\t': + /* Two dots then whitespace, it's a TCL one-shot command. */ + while( (c = *zScript)!=0 && c==' ' || c=='\t' ) ++zScript, --nc; + if ( c!=0 ) goto one_shot_tcl; + /* It looks like "..", so run it that way via fall-thru. */ + case 0: + /* Two lone dots, user wants to run TCL REPL. */ + return runTclREPL(interp, pzErrMsg); + default: + /* Two dots then dark not dot, may be a dot command. */ + if( *zScript>='a' && *zScript<='z' ){ + --zScript, ++nc; + goto one_shot_tcl; + } + /* It cannot be a dot command; a user tip is apparently needed. */ + if( pzErrMsg ){ + *pzErrMsg = sqlite3_mprintf("Nothing valid begins with ..%c\n" + "Run .help tcl to see what is valid.\n", + *zScript); + return DCR_SayUsage; + } + } + return DCR_Ok|(rc!=TCL_OK); + } + return DCR_Error; /* Silent error because it should not happen. */ +} + +DERIVED_METHOD(void, destruct, DotCommand,TclCmd, 0, ()){ + /* Nothing to do, instance data is static. */ + (void)(pThis); +} +static DERIVED_METHOD(void, destruct, DotCommand,UnkCmd, 0, ()); + +DERIVED_METHOD(void, destruct, ScriptSupport,TclSS, 0, ()){ + /* Nothing to do, instance data is static. */ + (void)(pThis); +} + +DERIVED_METHOD(const char *, name, DotCommand,TclCmd, 0,()){ + return "tcl"; +} +DERIVED_METHOD(const char *, name, DotCommand,UnkCmd, 0,()){ + return "unknown"; +} + +DERIVED_METHOD(const char *, help, DotCommand,TclCmd, 1,(const char *zHK)){ + (void)(pThis); + if( zHK==0 ) + return + ".tcl ?FILES? Run a TCL REPL or interpret files as TCL.\n"; + if( *zHK==0 ) + return + " If FILES are provided, they name files to be read in as TCL.\n" + " Otherwise, a read/evaluate/print loop is run until a lone \".\" is\n" + " entered as complete TCL input or input end-of-stream is encountered.\n" + "\n" + " The same REPL can be run with a lone \"..\". Or the \"..\" prefix\n" + " may be used thusly, \"..dotcmd ...\" or \".. tclcmd ...\", to run a\n" + " single dot command or TCL command, respectively, whereupon it will\n" + " be run in its respective execution environment after its arguments\n" + " are collected using TCL parsing rules and expanded as for TCL in\n" + " the TCL base namespace. In this way, arguments may be \"computed\".\n" + ; + return 0; +} + +DERIVED_METHOD(const char *, help, DotCommand,UnkCmd, 1,(const char *zHK)); + +DERIVED_METHOD(DotCmdRC, argsCheck, DotCommand,TclCmd, 3, + (char **pzErrMsg, int nArgs, char *azArgs[])){ + return DCR_Ok; +} +DERIVED_METHOD(DotCmdRC, argsCheck, DotCommand,UnkCmd, 3, + (char **pzErrMsg, int nArgs, char *azArgs[])){ + return DCR_Ok; +} + +static void copy_complaint(char **pzErr, Tcl_Interp *pi){ + if( pzErr ){ + Tcl_Obj *po = Tcl_GetObjResult(pi); + *pzErr = sqlite3_mprintf("%s\n", Tcl_GetStringFromObj(po,0)); + } +} + +/* The .tcl/.. REPL script is one of the 3 following string literals, + * selected at build time for these different purposes: + * 1st: a simple input collection, reading only stdin, which may + * be (handily) used as a fallback for debugging purposes. + * 2nd: input collection which honors the shell's input switching + * and otherwise has low dependency upon shell features, which + * means that it has no input line editing or history recall. + * 3rd: an input collection which fully leverages the shell's + * input collection. It has higher shell dependency, and for + * that it gains the shell's line editing and history recall, + * in addition to working with the shell's input switching. + * It also supports recursive REPLs when return is caught. + */ +#ifdef TCL_REPL_STDIN_ONLY +# define TCL_REPL 1 +#elif defined(TCL_REPL_LOW_DEPENDENCY) +# define TCL_REPL 2 +#else +# define TCL_REPL 3 +#endif + + +#if TCL_REPL==1 /* a fallback for debug */ +TCL_CSTR_LITERAL(static const char * const zREPL = ){ + set line {} + while {![eof stdin]} { + if {$line!=""} { + puts -nonewline "> " + } else { + puts -nonewline "% " + } + flush stdout + append line [gets stdin] + if {$line eq "."} break + if {[info complete $line]} { + if {[catch {uplevel #0 $line} result]} { + puts stderr "Error: $result" + } elseif {$result!=""} { + puts $result + } + set line {} + } else { + append line \n + } + } + if {$line ne "."} {puts {}} + read stdin 0 +}; +#elif TCL_REPL==2 /* minimal use of shell's read */ +TCL_CSTR_LITERAL(static const char * const zREPL = ){ + namespace eval ::REPL { + variable line {} + variable at_end 0 + variable prompting [now_interactive] + } + while {!$::REPL::at_end} { + if {$::REPL::prompting} { + if {$::REPL::line!=""} { + puts -nonewline "...> " + } else { + puts -nonewline "tcl% " + } + } + flush stdout + set ::REPL::li [get_input_line] + if {$::REPL::li eq ""} { + set ::REPL::at_end 1 + } elseif {[string trimright $::REPL::li] eq "."} { + if {$::REPL::line ne ""} { + throw {NONE} {incomplete input at EOF} + } + set ::REPL::at_end 1 + } else { + append ::REPL::line $::REPL::li + if {[string trim $::REPL::line] eq ""} { + set ::REPL::line "" + continue + } + if {[info complete $::REPL::line]} { + set ::REPL::rc [catch {uplevel #0 $::REPL::line} ::REPL::result] + if {$::REPL::rc == 0} { + if {$::REPL::result!="" && $::REPL::prompting} { + puts $::REPL::result + } + } elseif {$::REPL::rc == 1} { + puts stderr "Error: $::REPL::result" + } elseif {$::REPL::rc == 2} { + set ::REPL::at_end 1 + } + set ::REPL::line {} + } + } + } + if {$::REPL::prompting && $::REPL::li ne ".\n"} {puts {}} + namespace delete ::REPL + read stdin 0 +}; +#elif TCL_REPL==3 +/* using shell's input collection with line editing (if configured) */ +static const char * const zREPL = "uplevel #0 sqlite_shell_REPL"; + +TCL_CSTR_LITERAL(static const char * const zDefineREPL = ){ + proc sqlite_shell_REPL {} { + if {[info exists ::tcl_interactive]} { + set save_interactive $::tcl_interactive + } + set ::tcl_interactive [now_interactive] + while {1} { + foreach {group ready} [get_tcl_group] {} + set trimmed [string trim $group] + if {$group eq "" && !$ready} break + if {$trimmed eq ""} continue + if {!$ready && $trimmed ne ""} { + throw {NONE} {incomplete input at EOF} + } + if {$trimmed eq "."} break + set rc [catch {uplevel #0 $group} result] + if {$rc == 0} { + if {$result != "" && $::tcl_interactive} { + puts $result + } + } elseif {$rc == 1} { + puts stderr "Error: $result" + } elseif {$rc == 2} { + return -code 2 + } + } + if {$::tcl_interactive && $trimmed ne "."} {puts {}} + read stdin 0 + if {[info exists save_interactive]} { + set ::tcl_interactive $save_interactive + } else { unset ::tcl_interactive } + } +}; +#else + "throw {NONE} {not built for REPL}\n" +#endif + +/* Enter the preferred REPL */ +static DotCmdRC runTclREPL(Tcl_Interp *interp, char **pzErrMsg){ + int rc = Tcl_Eval(interp, zREPL); + clearerr(stdin); /* Cure issue where stdin gets stuck after keyboard EOF. */ + if( rc!=TCL_OK ){ + copy_complaint(pzErrMsg, interp); + return DCR_Error; + } + return DCR_Ok; +} + +DERIVED_METHOD(DotCmdRC, execute, DotCommand,TclCmd, 4, + (ShellExState *psx, char **pzErrMsg, int nArgs, char *azArgs[])){ + FILE *out = SHX_HELPER(currentOutputFile)(psx); + TclCmd *ptc = (TclCmd *)pThis; + if( nArgs>1 ){ + /* Read named files into the interpreter. */ + int rc = TCL_OK; + int aix; + for( aix=0; aix<(nArgs-1) && rc==TCL_OK; ++aix ){ + rc = Tcl_EvalFile(getInterp(), azArgs[aix+1]); + } + if( rc!=TCL_OK ){ + copy_complaint(pzErrMsg, getInterp()); + return DCR_Error; + } + return DCR_Ok; + }else{ + /* Enter a REPL */ + return runTclREPL(getInterp(), pzErrMsg); + } +} + +DERIVED_METHOD(DotCmdRC, execute, DotCommand,UnkCmd, 4, + (ShellExState *psx, char **pzErrMsg, int nArgs, char *azArgs[])){ + Tcl_Interp *interp = getInterp(); + Tcl_Obj **ppo; + char zName[50]; + int ia, rc; + + if( interp==0 || nArgs==0 ) return DCR_Unknown; + + sqlite3_snprintf(sizeof(zName), zName, ".%s", azArgs[0]); + if( !Tcl_FindCommand(interp, zName, 0, TCL_GLOBAL_ONLY|TCL_LEAVE_ERR_MSG) ){ + if( !SHX_HELPER(nowInteractive)(psx) ){ + *pzErrMsg = sqlite3_mprintf("Command %s not found.\n", zName); + return DCR_Unknown; + }else{ + FILE *out = SHX_HELPER(currentOutputFile)(psx); + fprintf(stderr, "The %s command does not yet exist.\n", zName); + fprintf(out, "Run .help to see existent dot commands," + " or create %s as a TCL proc.\n", zName); + return DCR_CmdErred; + } + } + ppo = sqlite3_malloc((nArgs+1)*sizeof(Tcl_Obj*)); + if( ppo==0 ) return TCL_ERROR; + for( ia=0; ia no input obtained and no more to be had + * { Other 0 } => input collected, but is invalid TCL + * { Other 1 } => input collected, may be valid TCL + * By design, this combination is never returned: + * { Empty 1 } => no input collected but valid TCL + */ +static int getTclGroup(void *pvSS, Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[]){ + if( objc==1 ){ + static Prompts cueTcl = { "tcl% ", " > " }; + ShellExState *psx = (ShellExState *)pvSS; + struct InSource *pis = SHX_HELPER(currentInputSource)(psx); + int isComplete = 0; + char *zIn = 0; + int isContinuation = 0; + do { + zIn = SHX_HELPER(oneInputLine)(pis, zIn, isContinuation, &cueTcl); + if( isContinuation ){ + if( zIn ){ + Tcl_AppendResult(interp, "\n", zIn, (char*)0); + isComplete = Tcl_CommandComplete(Tcl_GetStringResult(interp)); + } + }else if( zIn ){ + isComplete = Tcl_CommandComplete(zIn); + Tcl_SetResult(interp, zIn, TCL_VOLATILE); + } + isContinuation = 1; + } while( zIn && !isComplete ); + if( zIn ) SHX_HELPER(freeInputLine)(zIn); + { - Tcl_Obj *const objv[] = { ++ Tcl_Obj *const objvv[] = { + Tcl_NewStringObj(Tcl_GetStringResult(interp) , -1), + Tcl_NewIntObj(isComplete) + }; /* These unowned objects go directly into result, becoming owned. */ + Tcl_ResetResult(interp); - Tcl_SetObjResult(interp, Tcl_NewListObj(2, objv)); ++ Tcl_SetObjResult(interp, Tcl_NewListObj(2, objvv)); + } + return TCL_OK; + }else{ + Tcl_SetResult(interp, "too many arguments", TCL_STATIC); + return TCL_ERROR; + } +} +#endif + +/* C implementation of TCL proc, now_interactive */ +static int nowInteractive(void *pvSS, Tcl_Interp *interp, + int nArgs, const char *azArgs[]){ + if( nArgs==1 ){ + ShellExState *psx = (ShellExState *)pvSS; + struct InSource *pis = SHX_HELPER(currentInputSource)(psx); + static const char * zAns[2] = { "0","1" }; + int iiix = (SHX_HELPER(nowInteractive)(psx) != 0); + Tcl_SetResult(interp, (char *)zAns[iiix], TCL_STATIC); + return TCL_OK; + }else{ + Tcl_SetResult(interp, "too many arguments", TCL_STATIC); + return TCL_ERROR; + } +} + +#ifdef SHELL_ENABLE_TK +static int numEventLoops = 0; +static int inOuterLoop = 0; + +static int exitThisTkGUI(void *pvSS, Tcl_Interp *interp, + int nArgs, const char *azArgs[]){ + if( numEventLoops==0 && !inOuterLoop ){ + int ec = 0; + if( nArgs>=2 ){ + if( azArgs[1] && sscanf(azArgs[1], "%d", &ec)!=1 ){ + ec = 1; + fprintf(stderr, "Exit: %d\n", ec); + }else{ + const char *zA = (azArgs[1])? azArgs[1] : "null"; + fprintf(stderr, "Exit: \"%s\"\n", azArgs[1]); + } + }else{ + fprintf(stderr, "Exit without argument\n"); + } + fprintf(stderr, "Exit: \"%s\"\n", azArgs[1]); + // exit(ec); + } + --numEventLoops; + return TCL_BREAK; +} + +static void runTclEventLoop(void){ + int inOuter = inOuterLoop; + int nmw = Tk_GetNumMainWindows(); + /* This runs without looking at stdin. So it cannot be a REPL, yet. + * Unless user has created something for it to do, it does nothing. */ + /* Tk_MapWindow(Tk_MainWindow(interpKeep.pInterp)); */ + ++numEventLoops; + inOuterLoop = 1; + while( nmw > 0 ) { + Tcl_DoOneEvent(0); + nmw = Tk_GetNumMainWindows(); + /* if( nmw==1 ){ */ + /* Tk_UnmapWindow(Tk_MainWindow(interpKeep.pInterp)); */ + /* nmw = Tk_GetNumMainWindows(); */ + /* break; */ + /* } */ + } + if( nmw==0 ){ + fprintf(stderr, + "Tk application and its root window destroyed. Restarting Tk.\n"); + Tk_Init(interpKeep.pInterp); + } + --numEventLoops; + inOuterLoop = inOuter; +} + +static int runTkGUI(void *pvSS, Tcl_Interp *interp, + int nArgs, const char *azArgs[]){ + (void)(pvSS); /* ShellExState *psx = (ShellExState *)pvSS; */ + Tcl_SetMainLoop(runTclEventLoop); + runTclEventLoop(); + return TCL_OK; +} +#endif /* defined(SHELL_ENABLE_TK) */ + +#define UNKNOWN_RENAME "_original_unknown" + +/* C implementation of TCL ::register_adhoc_command name ?help? */ +static int registerAdHocCommand(/* ShellExState */ void *pv, + Tcl_Interp *interp, + int nArgs, const char *azArgs[]){ + ShellExState *psx = (ShellExState*)pv; + if( nArgs>3 ){ + Tcl_SetResult(interp, "too many arguments", TCL_STATIC); + }else if( nArgs<2 ){ + Tcl_SetResult(interp, "too few arguments", TCL_STATIC); + }else{ + const char *zHT = (nArgs==3)? azArgs[2] : 0; + Tcl_ResetResult(interp); + SHX_API(registerAdHocCommand)(psx, sqlite3_tclshext_init, azArgs[1], zHT); + return TCL_OK; + } + return TCL_ERROR; +} + +/* C implementation of TCL unknown to (maybe) delegate to dot commands */ +static int unknownDotDelegate(void *pvSS, Tcl_Interp *interp, + int nArgs, const char *azArgs[]){ + const char *name = (nArgs>1 && *azArgs[1]=='.')? azArgs[1]+1 : 0; + ShellExState *psx = (ShellExState *)pvSS; + DotCommand *pdc = 0; + int nFound = 0; + int ia, rc; + + if( name ) pdc = SHX_HELPER(findDotCommand)(name, psx, &nFound); + if( pdc==(DotCommand*)&tclcmd && nArgs==2 ){ + /* Will not do a nested REPL, just silently semi-fake it. */ + return TCL_OK; + } + if( pdc && nFound==1 ){ + /* Run the dot command and interpret its returns. */ + DotCmdRC drc = SHX_HELPER(runDotCommand)(pdc, (char **)azArgs+1, + nArgs-1, psx); + if( drc==DCR_Ok ) return TCL_OK; + else if( drc==DCR_Return ){ + return TCL_RETURN; + }else{ + Tcl_AppendResult(interp, "Execution of .", name, " failed.", 0); + return TCL_ERROR; + } + }else{ + /* Defer to the TCL-default unknown command, or fail here. */ + if( 0!=Tcl_FindCommand(interp, UNKNOWN_RENAME, 0, TCL_GLOBAL_ONLY) ){ + Tcl_Obj **ppo = sqlite3_malloc((nArgs+1)*sizeof(Tcl_Obj*)); + if( ppo==0 ) return TCL_ERROR; + ppo[0] = Tcl_NewStringObj(UNKNOWN_RENAME, -1); + Tcl_IncrRefCount(ppo[0]); + for( ia=1; iappSdb + = (SqliteDb**)Tcl_Realloc((char*)pudb->ppSdb, (pudb->numSdb+1)*sizeof(p)); + memset(pudb->ppSdb + pudb->numSdb, 0, sizeof(SqliteDb*)); + p = (SqliteDb*)Tcl_Alloc(sizeof(SqliteDb)); + memset(p, 0, sizeof(SqliteDb)); + pudb->ppSdb[pudb->numSdb] = p; + p->db = udb; + p->interp = pudb->interp; + p->maxStmt = NUM_PREPARED_STMTS; + p->openFlags = SQLITE_OPEN_URI; + p->nRef = 1; + return pudb->numSdb++; +} + +/* Remove a DB from the list */ +static void udbRemove(UserDb *pudb, int ix){ + SqliteDb *pdb; + assert(ix>=0 && ixnumSdb); + /* The code below is highly dependent upon implementation details of + * tclsqlite.c , and may become incorrect if that code changes. This + * is an accepted downside of reusing vast portions of that code. + * The minutiae in these comments is to explain the dependencies so + * that adjustments might be easily made when proven necessary. */ + pdb = pudb->ppSdb[ix]; +#ifndef SQLITE_OMIT_INCRBLOB + /* This is a preemptive action, which is normally done by the + * delDatabaseRef() routine, which needs a non-zero db pointer + * to reach Tcl_UnregisterChannel()'s implementation. We do it + * now because, to avoid closing that db, that pointer will be + * set to 0 when delDatabaseRef() runs. */ + closeIncrblobChannels(pdb); + /* Prevent closeIncrblobChannels() from trying to free anything. */ + pdb->pIncrblob = 0; +#endif + /* This appears to not be necessary; it is defensive in case the + * flushStmtCache() or dbFreeStmt() code begins to use pdb->db . + * We rely on its behavior whereby, once flushed, the cache is + * made to appear empty in the SqliteDb struct. */ + flushStmtCache(pdb); + /* This next intervention prevents delDatabaseRef() from closing + * the .db ; this relies on sqlite3_close(0) being a NOP. If the + * SqliteDb takedown code changes, this may lead to an address + * fault. For that reason, the *.in which produces this source + * should be tested by excercising the TCL udb command well. */ + pdb->db = 0; + assert(pdb->nRef==1); + /* Use the "stock" delete for sqlite3-generated objects. */ + delDatabaseRef(pdb); + /* At this point, pdb has been Tcl_Free()'ed. Forget it. */ + --pudb->numSdb; + { + int nshift = pudb->numSdb-ix; + if( nshift>0 ){ + memmove(pudb->ppSdb+ix, pudb->ppSdb+ix+1, nshift*sizeof(pdb)); + } + } + /* Adjust index to currently visible DB. */ + if( ix==pudb->ixuSdb ) pudb->ixuSdb = -1; + else if( ixixuSdb ) --pudb->ixuSdb; +} + +static struct UserDb *udbCreate(Tcl_Interp *interp, ShellExState *psx); + +/* Cleanup the UserDb singleton. Should only be done at shutdown. + * This routine is idempotent, and may be called redundantly. + */ +static void udbCleanup(UserDb *pudb){ + /* If this is called too early, when *pudb is still associated with + * active (not yet closed) SqliteDb objects, those will simply be + * orphaned and leaked. But this assert may make the error evident. */ + if( pudb==0 ) pudb = udbCreate(0, 0); + assert(pudb->numSdb==0); - pudb->numSdb==0; + if( pudb->ppSdb ) Tcl_Free((char*)pudb->ppSdb); + memset(pudb, 0, sizeof(UserDb)); + pudb->ixuSdb = -1; +} + +/* Hunt for given db in UserDb's list. Return its index if found, else -1. */ +static int udbIndexOfDb(UserDb *pudb, sqlite3 *psdb){ + int ix = 0; + while( ix < pudb->numSdb ){ + if( psdb==pudb->ppSdb[ix]->db ) return ix; + else ++ix; + } + return -1; +} + +/* The event handler used to keep udb command's wrapped DB in sync with + * changes to the ShellExState .dbUser member. This task is complicated + * by effects of these dot commands: .open ; .connection ; and .quit, + * .exit or various other shell exit causes. The intention is to always + * have an orderly and leak-free shutdown (excepting kill/OOM aborts.) + */ +static int udbEventHandle(void *pv, NoticeKind nk, void *pvSubject, + ShellExState *psx){ + UserDb *pudb = (UserDb*)pv; + if( nk==NK_ShutdownImminent ){ + udbCleanup(pudb); + }else if( nk==NK_Unsubscribe ){ + assert(pudb==0 || pudb->numSdb==0); + }else if( nk==NK_DbUserAppeared || nk==NK_DbUserVanishing + || nk==NK_DbAboutToClose || nk==NK_ExtensionUnload ){ + sqlite3 *dbSubject = (sqlite3*)pvSubject; + int ix = udbIndexOfDb(pudb, dbSubject); + switch( nk ){ + case NK_DbUserAppeared: + if( ix>=0 ) pudb->ixuSdb = ix; + else pudb->ixuSdb = udbAdd(pudb, dbSubject); + break; + case NK_DbUserVanishing: + if( ix>=0 ) pudb->ixuSdb = -1; + break; + case NK_ExtensionUnload: + SHX_API(subscribeEvents)(psx, sqlite3_tclshext_init, 0, + NK_Unsubscribe, udbEventHandle); + /* fall thru */ + case NK_DbAboutToClose: + if( ix>=0 ) udbRemove(pudb, ix); + break; + } + } + return 0; +} + +/* Create the UserDb object supporting TCL "udb" command operations. + * It's not wholly created because it is a singleton. Any subsequent + * creation is ignored; instead, the singleton is returned. This + * object is made to release resources only upon shutdown. If a TCL + * user removes the udb command, this avoids problems arising from + * this object holding references to databases that may still be in + * use, either as the active .dbUser or as a blob streaming store. */ +static struct UserDb *udbCreate(Tcl_Interp *interp, ShellExState *psx){ + static UserDb *rv = 0; + static UserDb udb = { 0 }; + if( interp==0 || psx==0 ) return &udb; + if( rv==0 ){ + sqlite3 *sdbS = psx->dbShell; + sqlite3 *sdbU = psx->dbUser; + rv = &udb; + rv->interp = interp; + rv->psx = psx; + rv->ppSdb = (SqliteDb**)Tcl_Alloc(6*sizeof(SqliteDb*)); + memset(rv->ppSdb, 0, 6*sizeof(SqliteDb*)); + assert(sdbS!=0); + udbAdd(rv, sdbS); + if( sdbU!=0 ){ + rv->ixuSdb = udbAdd(rv, sdbU); + } else rv->ixuSdb = -1; + rv->nRef = 1; + /* Arrange that this object tracks lifetimes and visibility of the + * ShellExState .dbUser member values which udb purports to wrap, + * and that shdb ceases wrapping the .dbShell member at shutdown. + * This subscription eventually leads to a udbCleanup() call. */ + SHX_API(subscribeEvents)(psx, sqlite3_tclshext_init, + rv, NK_CountOf, udbEventHandle); + } + return rv; +} + +static const char *azDbNames[] = { "shdb", "udb", 0 }; +static const int numDbNames = 2; + +/* C implementation behind added TCL udb command */ +static int UserDbObjCmd(void *cd, Tcl_Interp *interp, + int objc, Tcl_Obj * const * objv){ + UserDb *pudb = (UserDb*)cd; + static const char *azDoHere[] = { "close", 0 }; + enum DbDoWhat { DDW_Close }; + int doWhat; + int whichDb = -1; + const char *zMoan; + + if( Tcl_GetIndexFromObj(interp, objv[0], azDbNames, + "shell DB command", 0, &whichDb)){ + zMoan = " is not a wrapped DB.\n"; + goto complain_fail; + } + if( whichDb>0 ) whichDb = pudb->ixuSdb; + /* Delegate all subcommands except above to the now-visible SqliteDb. */ + if( objc>=2 && !Tcl_GetIndexFromObj(interp, objv[1], azDoHere, + "subcommand", 0, &doWhat)){ + switch( doWhat ){ + case DDW_Close: + zMoan = " close is disallowd. It is a wrapped DB.\n"; + goto complain_fail; + } + } + if( pudb->numSdb==0 || whichDb<0 ){ + zMoan = " references no DB yet.\n"; + goto complain_fail; + } + return DbObjCmd(pudb->ppSdb[whichDb], interp, objc, objv); + + complain_fail: + Tcl_AppendResult(interp, + Tcl_GetStringFromObj(objv[0], (int*)0), zMoan, (char*)0); + return TCL_ERROR; +} + +/* Get the udb command subsystem initialized and create "udb" TCL command. */ +static int userDbInit(Tcl_Interp *interp, ShellExState *psx){ + UserDb *pudb = udbCreate(interp, psx); + int nCreate = 0; + int ic; + for( ic=0; icnRef; + return TCL_OK; + } + return TCL_ERROR; +} + +/* +** Extension load function. +*/ +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_tclshext_init( + sqlite3 *db, + char **pzErrMsg, + const sqlite3_api_routines *pApi +){ + static const char * const azLoadFailures[] = { + "Extension load failed unexpectedly.", + "No ShellExtensionLink.\n Use '.load tclshext -shext' to load.", + "Outdated shell host extension API.\n Update the shell.", + "Outdated shell host helper API.\n Use a newer shell.", + }; + int iLoadStatus; + SQLITE_EXTENSION_INIT2(pApi); + SHELL_EXTENSION_INIT2(pShExtLink, shextLinkFetcher, db); + + SHELL_EXTENSION_INIT3(pShExtApi, pExtHelpers, pShExtLink); + iLoadStatus = SHELL_EXTENSION_LOADFAIL_WHY(pShExtLink, 6, 10); + if( iLoadStatus!=EXLD_Ok ){ + if( iLoadStatus>=sizeof(azLoadFailures)/sizeof(azLoadFailures[0]) ){ + iLoadStatus = 0; + } + *pzErrMsg = sqlite3_mprintf("%s\n", azLoadFailures[iLoadStatus]); + return SQLITE_ERROR; + }else{ + ShellExState *psx = pShExtLink->pSXS; + Tcl_Obj *targv = Tcl_NewListObj(0, NULL); + const char *zAppName = "tclshext"; + int tnarg = 0; +#ifdef SHELL_ENABLE_TK + int ldTk = 0; +#endif + int rc = 0; + + if( pShExtLink->nLoadArgs>0 ){ + int ila; + for( ila=0; ilanLoadArgs; ++ila ){ + const char *zA = pShExtLink->azLoadArgs[ila]; + if( strcmp(zA,"-tk")==0 ){ +#ifdef SHELL_ENABLE_TK + ldTk = 1; +#else + *pzErrMsg = sqlite3_mprintf("Option -tk not supported by this " + "tclshext extension as built.\n"); + return SQLITE_ERROR; +#endif + }else{ + /* Collect args not affecting init into the argv list. */ + Tcl_ListObjAppendElement(NULL, targv, Tcl_NewStringObj(zA, -1)); + ++tnarg; + } + } + } + rc = SHX_API(registerDotCommand)(psx, sqlite3_tclshext_init, + (DotCommand *)&unkcmd); + rc = SHX_API(registerDotCommand)(psx, sqlite3_tclshext_init, + (DotCommand *)&tclcmd); + if( rc==SQLITE_OK && + (rc = Tcl_BringUp( +#ifdef SHELL_ENABLE_TK + &ldTk, +#endif + pzErrMsg))==SQLITE_OK + ){ + Tcl_Interp *interp = getInterp(); + if( TCL_OK==userDbInit(interp, psx) ){ + UserDb *pudb = udbCreate(interp, psx); + pShExtLink->extensionDestruct = (void (*)(void*))udbCleanup; + pShExtLink->pvExtensionObject = pudb; + } + SHX_API(registerScripting)(psx, sqlite3_tclshext_init, + (ScriptSupport *)&tclss); +#if TCL_REPL==1 || TCL_REPL==2 + Tcl_CreateCommand(interp, "get_input_line", getInputLine, psx, 0); +#endif +#if TCL_REPL==3 + Tcl_CreateObjCommand(interp, "get_tcl_group", getTclGroup, psx, 0); + Tcl_Eval(interp, zDefineREPL); +#endif + Tcl_CreateCommand(interp, "now_interactive", nowInteractive, psx, 0); + /* Rename unknown so that calls to it can be intercepted. */ + Tcl_Eval(interp, "rename unknown "UNKNOWN_RENAME); + Tcl_CreateCommand(interp, "unknown", unknownDotDelegate, psx, 0); + /* Add a command to facilitate ad-hoc TCL dot commands showing up in + * the .help output with help text as specified by calling this: */ + Tcl_CreateCommand(interp, "register_adhoc_command", + registerAdHocCommand, (void*)psx, 0); + + /* Define this proc so that ".." either gets to the TCL REPL loop + * or does nothing (if already in it), as a user convenience. */ + Tcl_Eval(interp, "proc .. {} {}"); +#ifdef SHELL_ENABLE_TK + if( ldTk ){ + /* Create a proc to launch GUI programs, in faint mimicry of wish. + * + * Its first argument, pgmName is the name to be given to the GUI + * program that may be launched, used for error reporting and to + * become the value of the ::argv0 that it sees. + * + * Its second argument, pgmSetup, will be executed as a list (of + * a command and its arguments) to setup the GUI program. It may + * do anything necessary to prepare for the GUI program to be + * run by running a Tk event loop. It may be an empty list, in + * which case pgmName must name a list serving the same purpose. + * + * Subsequent arguments to this proc will be passed to the GUI + * program in the ::argv/::argc variable pair it sees. + * + * If only two empty arguments are provided to this proc, (whether + * as defaulted or explictly passed), the GUI event loop will be + * run with whatever conditions have been setup prior to the call. + * (This is perfectly legitimate; this "gui" proc provides a way + * to package GUI preparation and separate it from GUI run.) + * + * It is the responsibility of whatever setup code is run, if any, + * to leave Tk objects and variables set so that when a GUI event + * loop is run, some useful GUI program runs and can terminate. + * + * Before running the setup code, a variable, ::isHost, is set + * true to possibly inform the setup code that it should avoid + * exit and exec calls. Setup code which is designed for either + * hosted or standalone use, when run with $::isHost!=0, may opt + * to leave variables ::exitCode and ::resultValue set which are + * taken to indicate pseudo-exit status and a string result to + * be used for error reporting or possibly other purposes. + * + * If the above responsibilities cannot be met, setup code should + * fail in some way so that its execution produces a TCL error or + * follows the ::exitCode and ::resultValue convention. Otherwise, + * annoying sqlite3 shell hangs or abrupt exits may result. + */ + TCL_CSTR_LITERAL(const char * const zGui =){ + proc gui {{pgmName ""} {pgmSetup {}} args} { + unset -nocomplain ::exitCode + set ::tcl_interactive [now_interactive] + set saveArgs [list $::argv0 $::argc $::argv] + if {"$pgmName" ne ""} { + set ::argv0 $pgmName + } else {set ::argv0 "?"} + set ::argv $args + set ::argc [llength $args] + if {[llength $pgmSetup] == 0 && $pgmName ne ""} { + if { [catch {set ::programSetup [subst "\$$pgmName"]}] } { + foreach {::argv0 ::argc ::argv} $saveArgs {} + return -code 1 "Error: pgmSetup empty, and pgmName does not\ + name a list that might be\n executed in\ + its place. Consult tclshext doc on using the gui command." + } + } elseif {[llength $pgmSetup] == 0 && $pgmName eq ""} { + unset -nocomplain ::programSetup + } else { + set ::programSetup $pgmSetup + } + if {[info exists ::programSetup] && [llength $::programSetup] > 0} { + set rc [catch {uplevel #0 { + {*}$::programSetup + }} result options] + if {$rc==1} { + puts stderr "gui setup failed: $result" + puts stderr [dict get $options -errorinfo] + } elseif {[info exists ::exitCode] && $::exitCode!=0} { + puts stderr "gui setup failed: $::resultValue" + } else { run_gui_event_loop } + } else { + run_gui_event_loop + } + foreach {::argv0 ::argc ::argv} $saveArgs {} + } + }; + /* Create a command which nearly emuluates Tk_MainLoop(). It runs a + * GUI event loop, so does not return until either: all Tk top level + * windows are destroyed, which causes and error return, or the Tk + * app has called the replacement exit routine described next. */ + Tcl_CreateCommand(interp, "run_gui_event_loop", runTkGUI, psx, 0); + Tcl_Eval(interp, "rename exit process_exit"); + Tcl_CreateCommand(interp, "exit", exitThisTkGUI, psx, 0); + Tcl_Eval(interp, zGui); + Tcl_SetMainLoop(runTclEventLoop); + zAppName = "tclshext_tk"; + } +#endif /* ..TK */ + Tcl_SetVar2Ex(interp, "::argv0", NULL, + Tcl_NewStringObj(zAppName,-1), TCL_GLOBAL_ONLY); + Tcl_SetVar2Ex(interp, "::argc", NULL, + Tcl_NewIntObj(tnarg), TCL_GLOBAL_ONLY); + Tcl_SetVar2Ex(interp, "::argv", NULL, targv, TCL_GLOBAL_ONLY); + Tcl_SetVar2Ex(interp, "::tcl_interactive", NULL, + Tcl_NewIntObj(SHX_HELPER(nowInteractive)(psx)), + TCL_GLOBAL_ONLY); + Tcl_SetVar2Ex(interp, "::isHosted", NULL, + Tcl_NewIntObj(1), TCL_GLOBAL_ONLY); + pShExtLink->eid = sqlite3_tclshext_init; + } + if( rc==SQLITE_OK ){ + pShExtLink->extensionDestruct = Tcl_TakeDown; + pShExtLink->pvExtensionObject = &interpKeep; + }else{ + Tcl_TakeDown(&interpKeep); + } + return rc; + } +} diff --cc manifest index 4488c50091,4b561cb6fc..4b12955648 --- a/manifest +++ b/manifest @@@ -1,11 -1,11 +1,11 @@@ - C Get\sCLI\sconformed\sto\srevised\sdoc\sfor\sit,\sand\stake\srecent\sfixes. - D 2022-04-24T20:00:15.974 -C Fix\sa\sharmless\stypo\sin\sa\scomment. -D 2022-04-27T18:38:46.859 ++C Get\sMSVC\sbuild\sgoing\sfor\ssqlite3x.exe\sand\sextensions. ++D 2022-04-29T18:36:17.701 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 -F Makefile.in b210ad2733317f1a4353085dfb9d385ceec30b0e6a61d20a5accabecac6b1949 +F Makefile.in 3a2d4e3a13c497b24b92fa36e93f80dc49844f1ca62e5a7848b899afe7105a37 F Makefile.linux-gcc f609543700659711fbd230eced1f01353117621dccae7b9fb70daa64236c5241 - F Makefile.msc 689726a2785027e0eb34ea9ce8e67ac94bc4aebbaa6def20ddb6fa9f7b0c43b5 -F Makefile.msc b28a8a7a977e7312f6859f560348e1eb110c21bd6cf9fab0d16537c0a514eef3 ++F Makefile.msc 2c59950b86b58b5a4d78151883ee52a014f006d08ea18783c87509fddc3cde27 F README.md 2dd87a5c1d108b224921f3dd47dea567973f706e1f6959386282a626f459a70c F VERSION fa8e7d2d1cc962f9e14c6d410387cf75860ee139462763fda887c1be4261f824 F aclocal.m4 a5c22d164aff7ed549d53a90fa56d56955281f50 @@@ -330,7 -328,6 +330,7 @@@ F ext/misc/showauth.c 732578f0fe4ce42d5 F ext/misc/spellfix.c 94df9bbfa514a563c1484f684a2df3d128a2f7209a84ca3ca100c68a0163e29f F ext/misc/sqlar.c 0ace5d3c10fe736dc584bf1159a36b8e2e60fab309d310cd8a0eecd9036621b6 F ext/misc/stmt.c 35063044a388ead95557e4b84b89c1b93accc2f1c6ddea3f9710e8486a7af94a - F ext/misc/tclshext.c.in e0e4387b95410172385488feaea2f622ad1d398f1ac6a4275874791fbfd31d4e ++F ext/misc/tclshext.c.in 8c939f7048630f73608133e6873e2c963eb8bb4bdfdf65144a3c6f2ca3d62510 F ext/misc/templatevtab.c 8a16a91a5ceaccfcbd6aaaa56d46828806e460dd194965b3f77bf38f14b942c4 F ext/misc/totype.c fa4aedeb07f66169005dffa8de3b0a2b621779fd44f85c103228a42afa71853b F ext/misc/uint.c 053fed3bce2e89583afcd4bf804d75d659879bbcedac74d0fa9ed548839a030b @@@ -552,13 -549,12 +553,13 @@@ F src/pcache1.c 54881292a9a5db202b2c0ac F src/pragma.c d1aead03e8418ff586c7cfca344c50a914b8eb06abd841e8e91a982d823671da F src/pragma.h e690a356c18e98414d2e870ea791c1be1545a714ba623719deb63f7f226d8bb7 F src/prepare.c fd940149c691684e7c1073c3787a7170e44852b02d1275d2e30a5b58e89cfcaf - F src/printf.c 05d8dfd2018bc4fc3ddb8b37eb97ccef7abf985643fa1caebdcf2916ca90fa32 + F src/printf.c 512574910a45341c8ad244bd3d4939968ebdfde215645b676fff01cc46e90757 F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c - F src/resolve.c 18d99e7146852d6064559561769fcca0743eb32b14a97da6dbed373a30ee0e76 + F src/resolve.c f72bb13359dd5a74d440df25f320dc2c1baff5cde4fc9f0d1bc3feba90b8932a F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 - F src/select.c 7c106b3f36d483242b0a9c696614cd53d6f29e1ac81da6a3f0e9ea92f4211cc3 - F src/shell.c.in 6029fb2305e92ebbca8c3e531031239f310dcfad6801ddc4a57dfebd6efc87d3 + F src/select.c cc1a7581403fc074eee85283ba8d81de50a831ae175cb65a5751be00f621c0d5 -F src/shell.c.in ae0a6fae983caac6f8c824733f0599dfdf7b3a7e8efdef3cb5e3ab2e457ffc35 ++F src/shell.c.in e0064544eee8542d2f3dc9803c1ae08592a0d9d68cfa7cfb3054ad73f53d1d67 +F src/shext_linkage.h 0fe0dd831eb1e77b7ae8bc37e80811546531f82b17ef998e08960646cbaf5191 F src/sqlite.h.in 2a35f62185eb5e7ecc64a2f68442b538ce9be74f80f28a00abc24837edcf1c17 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h f49e28c25bd941e79794db5415fdf7b202deb3bc072ed6f1ed273d578703684e @@@ -607,8 -603,6 +608,8 @@@ F src/test_quota.h 2a8ad1952d1d2ca9af0c F src/test_rtree.c 671f3fae50ff116ef2e32a3bf1fe21b5615b4b7b F src/test_schema.c f5d6067dfc2f2845c4dd56df63e66ee826fb23877855c785f75cc2ca83fd0c1b F src/test_server.c a2615049954cbb9cfb4a62e18e2f0616e4dc38fe - F src/test_shellext_c.c b611e468bb99ba27df9211f8b6f1179cbd77f29390a0c7b846eb0d00e14dd1dc - F src/test_shellext_cpp.cpp d33cc8538ebe76f10114c8c9e5d65caa4e183fb461a4311145739f0affb15566 ++F src/test_shellext_c.c 966a97842ab65ffd64a62a9862073139652a4f772fd20f982583892e3c35b8c1 ++F src/test_shellext_cpp.cpp 21410915d2985b5e82f63c5b3cb417bc17430c6336c92a0b86c25f3775776130 F src/test_sqllog.c 540feaea7280cd5f926168aee9deb1065ae136d0bbbe7361e2ef3541783e187a F src/test_superlock.c 4839644b9201da822f181c5bc406c0b2385f672e F src/test_syscall.c 1073306ba2e9bfc886771871a13d3de281ed3939 @@@ -1777,8 -1774,8 +1783,8 @@@ F test/walslow.test c05c68d4dc2700a982f F test/walthread.test 14b20fcfa6ae152f5d8e12f5dc8a8a724b7ef189f5d8ef1e2ceab79f2af51747 F test/walvfs.test bccb3e0d235ef85e276f491d34db32c9ada1ea67be8d9f10aabe7b30319ec656 F test/wapp.tcl b440cd8cf57953d3a49e7ee81e6a18f18efdaf113b69f7d8482b0710a64566ec -F test/wapptest.tcl 899594e25684861d5b0c0880fb012364def50ef8097041b8ddf74be5ba7fa270 x +F test/wapptest.tcl c55a4669d02e982921f1dc37ababa21eb14123ec73a396692165e927c16ff061 x - F test/where.test f114842c1851d257a26770f2ad55119b084001c0e1b8c214f886f45152d37cd8 + F test/where.test 8c6bbd0cae8feae142a7946e3484a802fa566bacf38452b1c3e48cb77321f9a4 F test/where2.test 03c21a11e7b90e2845fc3c8b4002fc44cc2797fa74c86ee47d70bd7ea4f29ed6 F test/where3.test 5b4ffc0ac2ea0fe92f02b1244b7531522fe4d7bccf6fa8741d54e82c10e67753 F test/where4.test 4a371bfcc607f41d233701bdec33ac2972908ba8 @@@ -1954,10 -1951,8 +1960,8 @@@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a9 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 - P e21964480f68152d1c1607f75895354241ec3ea7247f1ee3c2a20c454beec919 - Q +77aed89192bdbad819ac17bf5d08728278a9b8cbbbef1d805df230caff79b417 - Q +dfd2100bc4f316825fd199b347849d1a2b941837f9eedcf36f3c3d280692b991 - R f2ef8541ab9fdfb6f43958528715f12d -P eb59c46a5aed69bc6fd096997bf24c082e533c1085439f6ec1fbe5ff78e8b374 -R f499c3fddca86db7a0183d3ecdb0953b -U drh -Z 7426364b9684e9a6fe822d40fffa4b25 ++P 60e85c7e7130724a426fab11552ed2aa8e9280f53a219c7e15e0ae0efcaded57 e1f4a115df34e45cf1bcf98961c699b582f564a58a979e95853b219bda06212c ++R 66992ed57c5d75ba400df4ac1d7afb51 +U larrybr - Z 8c1136d387cc01bce4a929f190c32a8e ++Z 08242a9a87ce842bca989bccc1923f6d # Remove this line to create a well-formed Fossil manifest. diff --cc manifest.uuid index 5e05ac9e52,1daa831936..d0097b95bf --- a/manifest.uuid +++ b/manifest.uuid @@@ -1,1 -1,1 +1,1 @@@ - 60e85c7e7130724a426fab11552ed2aa8e9280f53a219c7e15e0ae0efcaded57 -e1f4a115df34e45cf1bcf98961c699b582f564a58a979e95853b219bda06212c ++cd5e57e0fe4ef2a59c1202b66f4e774f6642fa826cfe3840a786104841dde2f6 diff --cc src/shell.c.in index a7d59fa94a,95a32f5247..6b333cbd75 --- a/src/shell.c.in +++ b/src/shell.c.in @@@ -1188,109 -1066,6 +1188,110 @@@ struct EQPGraph char zPrefix[100]; /* Graph prefix */ }; +/* By default, omit the extension options that are not done yet. + * "SHELL_EXTENSIONS" is short for "Some shell extensions are built in." */ +#ifndef SHELL_OMIT_EXTENSIONS +# define SHELL_OMIT_EXTENSIONS 4 +# define SHELL_EXTENSIONS 1 +#else +# define SHELL_EXTENSIONS \ + (0!=((~SHELL_OMIT_EXTENSIONS) & SHELL_ALL_EXTENSIONS)) +#endif + +#ifdef SQLITE_OMIT_LOAD_EXTENSION +# define SHELL_OMIT_LOAD_EXTENSION 1 +#else +# define SHELL_OMIT_LOAD_EXTENSION 0 +#endif + +/* Selectively omit features with one PP variable. Value is true iff +** either x is not defined or defined with 0 in bitnum bit position. +*/ +#define NOT_IFDEF_BIT(x,bitnum) (x? (!(x & (1<bExtendedDotCmds&(1<bExtendedDotCmds&(1<=0 && !IS_PATH_SEP(zPgm[ixe]) ) --ixe; + /* ixe is just before the basename with extension(s) */ + return sqlite3_strnicmp(&zPgm[ixe+1], "sqlite3x", 8)==0; +} +# else +# define isExtendedBasename(pathname) 0 +# endif + +/* Tracking and use info for loaded shell extensions + * An instance is kept for each shell extension that is currently loaded. + * They are kept in a simple list (aka dynamic array), index into which + * is used internally to get the extension's object. These indices are + * kept in the dbShell and updated there as the list content changes. + */ +typedef struct ShExtInfo { + ExtensionId extId; /* The xInit function pointer */ + void (*extDtor)(void *); /* Extension shutdown on exit or unload */ + void *pvExtObj; /* Passed to extDtor(...) at shutdown */ + /* Each shell extension library registers 0 or more of its extension + * implementations, interfaces to which are kept in below dynamic. + * arrays. The dbShell DB keeps indices into these arrays and into + * an array of this struct's instances to facilitate lookup by name + * of pointers to the implementations. */ + int numDotCommands; + DotCommand **ppDotCommands; + int numExportHandlers; + ExportHandler **ppExportHandlers; + int numImportHandlers; + ImportHandler **ppImportHandlers; + DotCommand *pUnknown; /* .unknown registered for this extension */ +} ShExtInfo; +#endif + /* Parameters affecting columnar mode result display (defaulting together) */ typedef struct ColModeOpts { int iWrap; /* In columnar modes, wrap lines reaching this limit */ @@@ -7362,30 -7217,31 +7363,7 @@@ static void *shellMalloc(int *pRc, sqli return pRet; } --/* - ** If pRc!=0 and *pRc is not SQLITE_OK when this function is called, it is a - ** no-op. Otherwise, zFmt is treated as a printf() style string. The result - ** of formatting it along with any trailing arguments is written into a -** If *pRc is not SQLITE_OK when this function is called, it is a no-op. -** Otherwise, zFmt is treated as a printf() style string. The result of -** formatting it along with any trailing arguments is written into a --** buffer obtained from sqlite3_malloc(), and pointer to which is returned. --** It is the responsibility of the caller to eventually free this buffer --** using a call to sqlite3_free(). - ** - ** If an OOM error occurs, (*pRc) is set to SQLITE_NOMEM (if pRc!=0) - ** and a NULL pointer returned. -** -** If an OOM error occurs, (*pRc) is set to SQLITE_NOMEM and a NULL -** pointer returned. --*/ --static char *shellMPrintf(int *pRc, const char *zFmt, ...){ -- char *z = 0; - if( pRc==0 || *pRc==SQLITE_OK ){ - if( *pRc==SQLITE_OK ){ -- va_list ap; -- va_start(ap, zFmt); -- z = sqlite3_vmprintf(zFmt, ap); -- va_end(ap); - if( z==0 && pRc!=0 ){ - if( z==0 ){ -- *pRc = SQLITE_NOMEM; -- } -- } -- return z; --} - ++static char *shellMPrintf(int *pRc, const char *zFmt, ...); /* ** When running the ".recover" command, each output table, and the special @@@ -7663,6700 -7518,3782 +7641,6724 @@@ static RecoverTable *recoverOrphanTable } return pTab; } - +#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */ + /* -** This function is called to recover data from the database. A script -** to construct a new database containing all recovered data is output -** on stream pState->out. ++** If pRc!=0 and *pRc is not SQLITE_OK when this function is called, it is a ++** no-op. Otherwise, zFmt is treated as a printf() style string. The result ++** of formatting it along with any trailing arguments is written into a ++** buffer obtained from sqlite3_malloc(), and pointer to which is returned. ++** It is the responsibility of the caller to eventually free this buffer ++** using a call to sqlite3_free(). ++** ++** If an OOM error occurs, (*pRc) is set to SQLITE_NOMEM (if pRc!=0) ++** and a NULL pointer returned. + */ -static int recoverDatabaseCmd(ShellState *pState, int nArg, char **azArg){ - int rc = SQLITE_OK; - sqlite3_stmt *pLoop = 0; /* Loop through all root pages */ - sqlite3_stmt *pPages = 0; /* Loop through all pages in a group */ - sqlite3_stmt *pCells = 0; /* Loop through all cells in a page */ - const char *zRecoveryDb = ""; /* Name of "recovery" database */ - const char *zLostAndFound = "lost_and_found"; ++static char *shellMPrintf(int *pRc, const char *zFmt, ...){ ++ char *z = 0; ++ if( pRc==0 || *pRc==SQLITE_OK ){ ++ va_list ap; ++ va_start(ap, zFmt); ++ z = sqlite3_vmprintf(zFmt, ap); ++ va_end(ap); ++ if( z==0 && pRc!=0 ){ ++ *pRc = SQLITE_NOMEM; ++ } ++ } ++ return z; ++} + +static DotCmdRC +writeDb( char *azArg[], int nArg, ShellExState *psx, char **pzErr ){ + int rc = 0; + const char *zDestFile = 0; + const char *zDb = 0; + sqlite3 *pDest; + sqlite3_backup *pBackup; + int j; + int bAsync = 0; + const char *zVfs = 0; + if( ISS(psx)->bSafeMode ) return DCR_AbortError; + for(j=1; j Maybe init db, add column zCol to it. + * zAutoColumn(0, &db, ?) => (db!=0) Form columns spec for CREATE TABLE, + * close db and set it to 0, and return the columns spec, to later + * be sqlite3_free()'ed by the caller. + * The return is 0 when either: + * (a) The db was not initialized and zCol==0 (There are no columns.) + * (b) zCol!=0 (Column was added, db initialized as needed.) + * The 3rd argument, pRenamed, references an out parameter. If the + * pointer is non-zero, its referent will be set to a summary of renames + * done if renaming was necessary, or set to 0 if none was done. The out + * string (if any) must be sqlite3_free()'ed by the caller. + */ +#ifdef SHELL_DEBUG +#define rc_err_oom_die(rc) \ + if( rc==SQLITE_NOMEM ) shell_check_oom(0); \ + else if(!(rc==SQLITE_OK||rc==SQLITE_DONE)) \ + fprintf(STD_ERR,"E:%d\n",rc), assert(0) +#else +static void rc_err_oom_die(int rc){ + if( rc==SQLITE_NOMEM ) shell_check_oom(0); + assert(rc==SQLITE_OK||rc==SQLITE_DONE); +} +#endif + +#ifdef SHELL_COLFIX_DB /* If this is set, the DB can be in a file. */ +static char zCOL_DB[] = SHELL_STRINGIFY(SHELL_COLFIX_DB); +#else /* Otherwise, memory is faster/better for the transient DB. */ +static const char *zCOL_DB = ":memory:"; +#endif + +/* Define character (as C string) to separate generated column ordinal + * from protected part of incoming column names. This defaults to "_" + * so that incoming column identifiers that did not need not be quoted + * remain usable without being quoted. It must be one character. + */ +#ifndef SHELL_AUTOCOLUMN_SEP +# define AUTOCOLUMN_SEP "_" +#else +# define AUTOCOLUMN_SEP SHELL_STRINGIFY(SHELL_AUTOCOLUMN_SEP) +#endif + +static char *zAutoColumn(const char *zColNew, sqlite3 **pDb, char **pzRenamed){ + /* Queries and D{D,M}L used here */ + static const char * const zTabMake = "\ +CREATE TABLE ColNames(\ + cpos INTEGER PRIMARY KEY,\ + name TEXT, nlen INT, chop INT, reps INT, suff TEXT);\ +CREATE VIEW RepeatedNames AS \ +SELECT DISTINCT t.name FROM ColNames t \ +WHERE t.name COLLATE NOCASE IN (\ + SELECT o.name FROM ColNames o WHERE o.cpos<>t.cpos\ +);\ +"; + static const char * const zTabFill = "\ +INSERT INTO ColNames(name,nlen,chop,reps,suff)\ + VALUES(iif(length(?1)>0,?1,'?'),max(length(?1),1),0,0,'')\ +"; + static const char * const zHasDupes = "\ +SELECT count(DISTINCT (substring(name,1,nlen-chop)||suff) COLLATE NOCASE)\ + 1, printf('%c%0*d', '"AUTOCOLUMN_SEP"', $1, cpos), '')" +#else /* ...RENAME_MINIMAL_ONE_PASS */ +"WITH Lzn(nlz) AS (" /* Find minimum extraneous leading 0's for uniqueness */ +" SELECT 0 AS nlz" +" UNION" +" SELECT nlz+1 AS nlz FROM Lzn" +" WHERE EXISTS(" +" SELECT 1" +" FROM ColNames t, ColNames o" +" WHERE" +" iif(t.name IN (SELECT * FROM RepeatedNames)," +" printf('%s"AUTOCOLUMN_SEP"%s'," +" t.name, substring(printf('%.*c%0.*d',nlz+1,'0',$1,t.cpos),2))," +" t.name" +" )" +" =" +" iif(o.name IN (SELECT * FROM RepeatedNames)," +" printf('%s"AUTOCOLUMN_SEP"%s'," +" o.name, substring(printf('%.*c%0.*d',nlz+1,'0',$1,o.cpos),2))," +" o.name" +" )" +" COLLATE NOCASE" +" AND o.cpos<>t.cpos" +" GROUP BY t.cpos" +" )" +") UPDATE Colnames AS t SET" +" chop = 0," /* No chopping, never touch incoming names. */ +" suff = iif(name IN (SELECT * FROM RepeatedNames)," +" printf('"AUTOCOLUMN_SEP"%s', substring(" +" printf('%.*c%0.*d',(SELECT max(nlz) FROM Lzn)+1,'0',1,t.cpos),2))," +" ''" +" )" +#endif + ; + static const char * const zCollectVar = "\ +SELECT\ + '('||x'0a'\ + || group_concat(\ + cname||' TEXT',\ + ','||iif((cpos-1)%4>0, ' ', x'0a'||' '))\ + ||')' AS ColsSpec \ +FROM (\ + SELECT cpos, printf('\"%w\"',printf('%!.*s%s', nlen-chop,name,suff)) AS cname \ + FROM ColNames ORDER BY cpos\ +)"; + static const char * const zRenamesDone = + "SELECT group_concat(" + " printf('\"%w\" to \"%w\"',name,printf('%!.*s%s', nlen-chop, name, suff))," + " ','||x'0a')" + "FROM ColNames WHERE suff<>'' OR chop!=0" + ; + int rc; + sqlite3_stmt *pStmt = 0; + assert(pDb!=0); + if( zColNew ){ + /* Add initial or additional column. Init db if necessary. */ + if( *pDb==0 ){ + if( SQLITE_OK!=sqlite3_open(zCOL_DB, pDb) ) return 0; +#ifdef SHELL_COLFIX_DB + if(*zCOL_DB!=':') + sqlite3_exec(*pDb,"drop table if exists ColNames;" + "drop view if exists RepeatedNames;",0,0,0); +#endif + rc = sqlite3_exec(*pDb, zTabMake, 0, 0, 0); + rc_err_oom_die(rc); + } + assert(*pDb!=0); + rc = sqlite3_prepare_v2(*pDb, zTabFill, -1, &pStmt, 0); + rc_err_oom_die(rc); + rc = sqlite3_bind_text(pStmt, 1, zColNew, -1, 0); + rc_err_oom_die(rc); + rc = sqlite3_step(pStmt); + rc_err_oom_die(rc); + sqlite3_finalize(pStmt); + return 0; + }else if( *pDb==0 ){ + return 0; + }else{ + /* Formulate the columns spec, close the DB, zero *pDb. */ + char *zColsSpec = 0; + int hasDupes = db_int(*pDb, zHasDupes); + int nDigits = (hasDupes)? db_int(*pDb, zColDigits) : 0; + if( hasDupes ){ +#ifdef SHELL_COLUMN_RENAME_CLEAN + 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); + assert(rc==SQLITE_DONE); + } + /* This assert is maybe overly cautious for above de-dup DML, but that can + * be replaced via #define's. So this check is made for debug builds. */ + assert(db_int(*pDb, zHasDupes)==0); + rc = sqlite3_prepare_v2(*pDb, zCollectVar, -1, &pStmt, 0); + rc_err_oom_die(rc); + rc = sqlite3_step(pStmt); + if( rc==SQLITE_ROW ){ + zColsSpec = smprintf("%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 = smprintf("%s", sqlite3_column_text(pStmt, 0)); + }else + *pzRenamed = 0; + } + } + sqlite3_finalize(pStmt); + sqlite3_close(*pDb); + *pDb = 0; + return zColsSpec; + } +} + +static FILE *currentOutputFile(ShellExState *p){ + return ISS(p)->out; +} + +#if SHELL_DYNAMIC_EXTENSION +/* Ensure there is room in loaded extension info list for one being loaded. + * On exit, psi->ixExtPending can be used to index into psi->pShxLoaded. + */ +static ShExtInfo *pending_ext_info(ShellInState *psi){ + int ixpe = psi->ixExtPending; + assert(ixpe!=0); + if( ixpe >= psi->numExtLoaded ){ + psi->pShxLoaded = sqlite3_realloc(psi->pShxLoaded, + (ixpe+1)*sizeof(ShExtInfo)); + shell_check_oom(psi->pShxLoaded); + ++psi->numExtLoaded; + memset(psi->pShxLoaded+ixpe, 0, sizeof(ShExtInfo)); + } + return &psi->pShxLoaded[ixpe]; +} + +/* Register a dot-command, to be called during extension load/init. */ +static int register_dot_command(ShellExState *p, + ExtensionId eid, DotCommand *pMC){ + ShellInState *psi = ISS(p); + ShExtInfo *psei = pending_ext_info(psi); + const char *zSql + = "INSERT INTO "SHELL_DISP_TAB"(name, extIx, cmdIx) VALUES(?, ?, ?)"; + int ie = psi->ixExtPending; + assert(psi->pShxLoaded!=0 && p->dbShell!=0); + if( pMC==0 ) return SQLITE_ERROR; + else{ + const char *zName = pMC->pMethods->name(pMC); + sqlite3_stmt *pStmt; + int nc = psei->numDotCommands; + int rc; + if( psei->extId!=0 && psei->extId!=eid ) return SQLITE_MISUSE; + psei->extId = eid; + rc = sqlite3_prepare_v2(p->dbShell, zSql, -1, &pStmt, 0); + if( rc!=SQLITE_OK ) return rc; + psei->ppDotCommands + = sqlite3_realloc(psei->ppDotCommands, (nc+1)*sizeof(DotCommand *)); + shell_check_oom(psei->ppDotCommands); + sqlite3_bind_text(pStmt, 1, zName, -1, 0); + sqlite3_bind_int(pStmt, 2, ie); + sqlite3_bind_int(pStmt, 3, nc); + rc = sqlite3_step(pStmt); + sqlite3_finalize(pStmt); + if( rc==SQLITE_DONE ){ + psei->ppDotCommands[nc++] = pMC; + psei->numDotCommands = nc; + notify_subscribers(psi, NK_NewDotCommand, pMC); + if( strcmp("unknown", zName)==0 ){ + psi->pUnknown = pMC; + psei->pUnknown = pMC; + } + return SQLITE_OK; + }else{ + psei->ppDotCommands[nc] = 0; + } + } + return SQLITE_ERROR; +} + +/* Register an output data display (or other disposition) mode */ +static int register_exporter(ShellExState *p, + ExtensionId eid, ExportHandler *pEH){ + return SQLITE_ERROR; +} + +/* Register an import variation from (various sources) for .import */ +static int register_importer(ShellExState *p, + ExtensionId eid, ImportHandler *pIH){ + return SQLITE_ERROR; +} + +/* See registerScripting API in shext_linkage.h */ +static int register_scripting(ShellExState *p, ExtensionId eid, + ScriptSupport *pSS){ + ShellInState *psi = ISS(p); + if( psi->scriptXid!=0 || psi->script!=0 ){ + /* Scripting support already provided. Only one provider is allowed. */ + return SQLITE_BUSY; + } + if( eid==0 || pSS==0 || psi->ixExtPending==0 ){ + /* Scripting addition allowed only when sqlite3_*_init() runs. */ + return SQLITE_MISUSE; + } + psi->script = pSS; + psi->scriptXid = eid; + return SQLITE_OK; +} + +/* See registerAdHocCommand API in shext_linkage.h re detailed behavior. + * Depending on zHelp==0, either register or unregister ad-hoc treatment + * of zName for this extension (identified by eid.) + */ +static int register_adhoc_command(ShellExState *p, ExtensionId eid, + const char *zName, const char *zHelp){ + ShellInState *psi = ISS(p); + u8 bRegNotRemove = zHelp!=0; + const char *zSql = bRegNotRemove + ? "INSERT OR REPLACE INTO "SHELL_AHELP_TAB + "(name, extIx, helpText) VALUES(?, ?, ?||?||?)" + : "DELETE FROM "SHELL_AHELP_TAB" WHERE name=? AND extIx=?"; + sqlite3_stmt *pStmt; + int rc, ie; + + assert(psi->pShxLoaded!=0 && p->dbShell!=0); + for( ie=psi->numExtLoaded-1; ie>0; --ie ){ + if( psi->pShxLoaded[ie].extId==eid ) break; + } + if( !zName || ie==0 || psi->pShxLoaded[ie].pUnknown==0 ) return SQLITE_MISUSE; + rc = sqlite3_prepare_v2(p->dbShell, zSql, -1, &pStmt, 0); + if( rc!=SQLITE_OK ) return rc; + sqlite3_bind_text(pStmt, 1, zName, -1, 0); + sqlite3_bind_int(pStmt, 2, ie); + if( bRegNotRemove ){ + int nc = strlen30(zHelp); + char cLead = *zHelp; + /* Add leading '.' if no help classifier present. */ + const char *zCL = (cLead!='.' && cLead!=',')? "." : ""; + /* Add trailing newline if not already there. */ + const char *zLE = (nc>0 && zHelp[nc-1]!='\n')? "\n" : ""; + sqlite3_bind_text(pStmt, 3, zCL, -1, 0); + sqlite3_bind_text(pStmt, 4, zHelp, -1, 0); + sqlite3_bind_text(pStmt, 5, zLE, -1, 0); + } + rc = sqlite3_step(pStmt); + sqlite3_finalize(pStmt); + return (rc==SQLITE_DONE)? SQLITE_OK : SQLITE_ERROR; +} + +/* + * Subscribe to (or unsubscribe from) messages about various changes. + * Unsubscribe, effected when nkMin==NK_Unsubscribe, always succeeds. + * Return SQLITE_OK on success, or one of these error codes: + * SQLITE_ERROR when the nkMin value is unsupported by this host; + * SQLITE_NOMEM when a required allocation failed; or + * SQLITE_MISUSE when the provided eid or eventHandler is invalid. + */ +static int subscribe_events(ShellExState *p, ExtensionId eid, void *pvUserData, + NoticeKind nkMin, ShellEventNotify eventHandler){ + ShellInState *psi = ISS(p); + struct EventSubscription *pes = psi->pSubscriptions; + struct EventSubscription *pesLim = pes + psi->numSubscriptions; + if( nkMin==NK_Unsubscribe ){ + /* unsubscribe (if now subscribed) */ + while( pes < pesLim ){ + if( (eventHandler==0 || eventHandler==pes->eventHandler) + && (pes->eid==0 || pes->eid==eid) + && (eid!=0 || eventHandler!=0 ||/*for shell use*/ pvUserData==p ) ){ + int nLeft = pesLim - pes; + assert(pes->eventHandler!=0); + pes->eventHandler(pes->pvUserData, NK_Unsubscribe, pes->eid, p); + if( nLeft>1 ) memmove(pes, pes+1, (nLeft-1)*sizeof(*pes)); + --pesLim; + --psi->numSubscriptions; + }else{ + ++pes; + } + } + if( psi->numSubscriptions==0 ){ + sqlite3_free(psi->pSubscriptions); + psi->pSubscriptions = 0; + } + return SQLITE_OK; + }else{ + /* subscribe only if minimum NoticeKind supported by this host */ + if( nkMin > NK_CountOf ) return SQLITE_ERROR; + if( eventHandler==0 || eid==0 ) return SQLITE_MISUSE; + while( pes < pesLim ){ + /* Never add duplicate handlers, but may renew their user data. */ + if( pes->eid==eid && pes->eventHandler==eventHandler ){ + pes->pvUserData = pvUserData; + return SQLITE_OK; + } + ++pes; + } + assert(pes==pesLim); + pes = sqlite3_realloc(psi->pSubscriptions, + (psi->numSubscriptions+1)*sizeof(*pes)); + if( pes==0 ) return SQLITE_NOMEM; + psi->pSubscriptions = pes; + pes += (psi->numSubscriptions++); + pes->eid = eid; + pes->pvUserData = pvUserData; + pes->eventHandler = eventHandler; + return SQLITE_OK; + } +} + +/* + * Unsubscribe all event listeners having an ExtensionId > 0. This is + * done just prior closing the shell DB (when dynamic extensions will + * be unloaded and accessing them in any way is good for a crash.) + */ +static void unsubscribe_extensions(ShellInState *psi){ + ShellExState *psx = XSS(psi); + int esix = 0; + + if( psi->numExtLoaded<=1 ) return; /* Ignore shell pseudo-extension. */ + while( esixnumSubscriptions ){ + struct EventSubscription *pes = psi->pSubscriptions+esix; + if( pes->eid > 0 ){ + int nsin = psi->numSubscriptions; + subscribe_events(psx, pes->eid, psx, NK_Unsubscribe, 0); + esix = esix + 1 + (psi->numSubscriptions - nsin); + }else ++esix; + } +} + +static struct InSource *currentInputSource(ShellExState *p){ + return ISS(p)->pInSource; +} + +static int nowInteractive(ShellExState *p){ + return INSOURCE_IS_INTERACTIVE(ISS(p)->pInSource); +} + +static const char *shellInvokedAs(void){ + return Argv0; +} + +static const char *shellStartupDir(void){ + return startupDir; +} + +static void setColumnWidths(ShellExState *p, char *azWidths[], int nWidths); +static DotCommand * findDotCommand(const char *, ShellExState *, int *); +static DotCmdRC runDotCommand(DotCommand*, char *[], int na, ShellExState*); + +static ExtensionHelpers extHelpers = { + 13, + { + failIfSafeMode, + currentOutputFile, + currentInputSource, + strLineGet, + findDotCommand, + runDotCommand, + setColumnWidths, + nowInteractive, + shellInvokedAs, + shellStartupDir, + one_input_line, + free_input_line, + sqlite3_enable_load_extension, + 0 + } +}; + +static ShellExtensionAPI shellExtAPI = { + &extHelpers, 6, { + register_dot_command, + register_exporter, + register_importer, + register_scripting, + subscribe_events, + register_adhoc_command, + 0 + } +}; + +/* This SQL function provides a way for a just-loaded shell extension to + * obtain a ShellExtensionLink pointer from the shell core while using + * the same sqlite3_load_extension API used for SQLite extensions. + * + * (It is also useful for debugging a shell extension, as a breakpoint + * on it will be hit soon after loading and before real work is done.) + */ +static void shell_linkage( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + int linkKind = 0; + void *pv; + if( argc>0 ){ + linkKind = sqlite3_value_int(argv[0]); + } + switch (linkKind){ + case 0: + pv = sqlite3_user_data(context); + break; + case 1: + pv = &extHelpers; + break; + case 2: + pv = &shellExtAPI; + break; + default: + pv = 0; + } + if( pv==0 ) sqlite3_result_null(context); + else sqlite3_result_pointer(context, pv, SHELLEXT_API_POINTERS, 0); +} + +/* Do the initialization needed for use of dbShell for command lookup + * and dispatch and for I/O handler lookup and dispatch. + */ +static int begin_db_dispatch(ShellExState *psx){ + ShellInState *psi = ISS(psx); + sqlite3_stmt *pStmt = 0; + int ic, rc1, rc2; + int rc = 0; + char *zErr = 0; + const char *zSql; + ShExtInfo sei = {0}; + /* Consider: Store these dynamic arrays in the DB as indexed-into blobs. */ + assert(psx->dbShell==0 || (psi->numExtLoaded==0 && psi->pShxLoaded==0)); + rc = ensure_shell_db(psx); + if( rc!=SQLITE_OK ){ + utf8_printf(STD_ERR, "Error: Shell DB uncreatable. Best to .quit soon.\n"); + return SQLITE_ERROR; + } + if( ensure_dispatch_table(psx)!=SQLITE_OK ) return 1; + + psi->pShxLoaded = (ShExtInfo *)sqlite3_malloc(2*sizeof(ShExtInfo)); + sei.ppDotCommands + = (DotCommand **)sqlite3_malloc((numCommands+2)*sizeof(DotCommand *)); + sei.ppExportHandlers + = (ExportHandler **)sqlite3_malloc(2*sizeof(ExportHandler *)); + sei.ppImportHandlers + = (ImportHandler **)sqlite3_malloc(2*sizeof(ImportHandler *)); + if( sei.ppDotCommands==0||sei.ppExportHandlers==0||sei.ppImportHandlers==0 + || psi->pShxLoaded==0 ){ + shell_out_of_memory(); + } + sei.numExportHandlers = 0; + sei.numImportHandlers = 0; - for( ic=0; icpShxLoaded[psi->numExtLoaded++] = sei; + zSql = "INSERT INTO "SHELL_DISP_TAB"(name, extIx, cmdIx) VALUES(?, 0, ?)"; + rc1 = sqlite3_prepare_v2(psx->dbShell, zSql, -1, &pStmt, 0); + rc2 = sqlite3_exec(psx->dbShell, "BEGIN TRANSACTION", 0, 0, &zErr); + if( rc1!=SQLITE_OK || rc2!=SQLITE_OK ) return 1; + assert(sei.numDotCommands>0); + for( ic=0; icpMethods->name(pmc); + sqlite3_reset(pStmt); + sqlite3_bind_text(pStmt, 1, zName, -1, 0); + sqlite3_bind_int(pStmt, 2, ic); + rc = sqlite3_step(pStmt); + if( rc!=SQLITE_DONE ){ + sqlite3_exec(psx->dbShell, "ABORT", 0, 0, 0); + break; + } + } + sqlite3_finalize(pStmt); + if( rc!=SQLITE_DONE ) return 1; + rc = sqlite3_exec(psx->dbShell, "COMMIT", 0, 0, &zErr); + sqlite3_enable_load_extension(psx->dbShell, 1); + sqlite3_free(zErr); + psi->bDbDispatch = 1; + + return SQLITE_OK; +} + +/* Call one loaded extension's destructors, in reverse order + * of their objects' creation, then free the tracking dyna-arrays. + */ +static void free_one_shext_tracking(ShExtInfo *psei){ + int j; + if( psei->ppDotCommands!=0 ){ + for( j=psei->numDotCommands; j>0; --j ){ + DotCommand *pmc = psei->ppDotCommands[j-1]; + if( pmc->pMethods->destruct!=0 ) pmc->pMethods->destruct(pmc); + } + sqlite3_free(psei->ppDotCommands); + } + if( psei->ppExportHandlers!=0 ){ + for( j=psei->numExportHandlers; j>0; --j ){ + ExportHandler *peh = psei->ppExportHandlers[j-1]; + if( peh->pMethods->destruct!=0 ) peh->pMethods->destruct(peh); + } + sqlite3_free(psei->ppExportHandlers); + } + if( psei->ppImportHandlers!=0 ){ + for( j=psei->numImportHandlers; j>0; --j ){ + ImportHandler *pih = psei->ppImportHandlers[j-1]; + if( pih->pMethods->destruct!=0 ) pih->pMethods->destruct(pih); + } + sqlite3_free(psei->ppImportHandlers); + } + if( psei->extDtor!=0 ){ + psei->extDtor(psei->pvExtObj); + } +} + +/* Call all existent loaded extension destructors, in reverse order of their + * objects' creation, except for scripting support which is done last, + * then free the tracking dyna-arrays. + */ +static void free_all_shext_tracking(ShellInState *psi){ + if( psi->pShxLoaded!=0 ){ + int i = psi->numExtLoaded; + while( i>1 ){ + ShExtInfo *psei = &psi->pShxLoaded[--i]; + free_one_shext_tracking(psei); + if( psi->scriptXid!=0 && psi->scriptXid==psei->extId ){ + assert(psi->script!=0); + if (psi->script->pMethods->destruct){ + psi->script->pMethods->destruct(psi->script); + } + psi->script = 0; + psi->scriptXid = 0; + } + } + sqlite3_free(psi->pShxLoaded); + psi->pShxLoaded = 0; + psi->numExtLoaded = 0; + } +} + +static DotCommand *command_by_index(ShellInState *psi, int extIx, int cmdIx){ + assert(extIx>=0); + if( extIx>=0 && extIxnumExtLoaded ){ + ShExtInfo *psei = & psi->pShxLoaded[extIx]; + if( cmdIx>=0 && cmdIxnumDotCommands ){ + return psei->ppDotCommands[cmdIx]; + } + } + return 0; +} + +static int load_shell_extension(ShellExState *psx, const char *zFile, + const char *zProc, char **pzErr, + int nLoadArgs, char **azLoadArgs){ + ShellExtensionLink shxLink = { + sizeof(ShellExtensionLink), + &shellExtAPI, + psx, /* pSXS */ + 0, /* zErrMsg */ + 0, /* ExtensionId */ + 0, /* Extension destructor */ + 0, /* Extension data ref */ + nLoadArgs, azLoadArgs /* like-named members */ + }; //extDtor(pvExtObj) + ShellInState *psi = ISS(psx); + /* save script support state for possible fallback if load fails */ + ScriptSupport *pssSave = psi->script; + ExtensionId ssiSave = psi->scriptXid; + int rc; + + if( pzErr ) *pzErr = 0; + if( psx->dbShell==0 || ISS(psx)->numExtLoaded==0 ){ + rc = begin_db_dispatch(psx); + if( rc!=SQLITE_OK ) return rc; + assert(ISS(psx)->numExtLoaded==1 && psx->dbShell!=0); + } + psi->ixExtPending = psi->numExtLoaded; + sqlite3_create_function(psx->dbShell, "shext_pointer", 1, + SQLITE_DIRECTONLY|SQLITE_UTF8, + &shxLink, shell_linkage, 0, 0); + rc = sqlite3_load_extension(psx->dbShell, zFile, zProc, &shxLink.zErrMsg); + sqlite3_create_function(psx->dbShell, "shext_pointer", 1, + SQLITE_DIRECTONLY|SQLITE_UTF8, + 0, 0, 0, 0); /* deregister */ + if( pzErr!=0 ) *pzErr = shxLink.zErrMsg; + if( rc==SQLITE_OK ){ + /* Keep extension's id and destructor for later disposal. */ + ShExtInfo *psei = pending_ext_info(psi); + if( psei->extId!=0 && psei->extId!=shxLink.eid ) rc = SQLITE_MISUSE; + psei->extId = shxLink.eid; + psei->extDtor = shxLink.extensionDestruct; + psei->pvExtObj = shxLink.pvExtensionObject; + }else{ + /* Release all resources extension might have registered before failing. */ + if( psi->ixExtPending < psi->numExtLoaded ){ + free_one_shext_tracking(psi->pShxLoaded+psi->ixExtPending); + --psi->numExtLoaded; + } + /* And make it unwind any scripting linkage it might have setup. */ + if( psi->script!=0 ) psi->script->pMethods->destruct(psi->script); + psi->script = pssSave; + psi->scriptXid = ssiSave; + } + psi->ixExtPending = 0; + if( rc!=SQLITE_OK ){ + if( rc==SQLITE_MISUSE && pzErr!=0 ){ + *pzErr = sqlite3_mprintf("extension id mismatch %z\n", *pzErr); + } + rc = SQLITE_ERROR; + } + return rc; +} +#endif + +/* Dot-command implementation functions are defined in this section. +COMMENT Define dot-commands and provide for their dispatch and .help text. +COMMENT These should be kept in command name order for coding convenience +COMMENT except where dot-commands share implementation. (The ordering +COMMENT required for dispatch and help text is effected regardless.) The +COMMENT effect of this configuration can be seen in generated output or by +COMMENT executing tool/mkshellc.tcl --parameters (or --details or --help). +COMMENT Generally, this section defines dispatchable functions inline and +COMMENT causes collection of command_table entry initializers, to be later +COMMENT emitted by a mkshellc macro. (See EMIT_DOTCMD_INIT further on.) +** All dispatchable dot-command execute functions have this signature: +static int someCommand(char *azArg[], int nArg, ShellExState *p, char **pzErr); +*/ +DISPATCH_CONFIG[ + RETURN_TYPE=DotCmdRC + STORAGE_CLASS=static + ARGS_SIGNATURE=char *$arg4\[\], int $arg5, ShellExState *$arg6, char **$arg7 + DISPATCH_ENTRY={ "$cmd", ${cmd}Command, $arg1, $arg2, $arg3 }, - DOTCMD_INIT=DOT_CMD_INFO(${cmd}, $arg1,$arg2,$arg3, , ), ++ DOTCMD_INIT={ DOT_CMD_INFO(${cmd}, $arg1,$arg2,$arg3), {, }, 0 }, + CMD_CAPTURE_RE=^\s*{\s*"(\w+)" + DISPATCHEE_NAME=${cmd}Command + DC_ARG1_DEFAULT=[string length $cmd] + DC_ARG2_DEFAULT=0 + DC_ARG3_DEFAULT=0 + DC_ARG4_DEFAULT=azArg + DC_ARG5_DEFAULT=nArg + DC_ARG6_DEFAULT=p + DC_ARG7_DEFAULT=pzErr + DC_ARG_COUNT=8 +]; + +CONDITION_COMMAND(seeargs !defined(SHELL_OMIT_SEEARGS)); +/***************** + * The .seeargs command + */ +COLLECT_HELP_TEXT[ + ",seeargs Echo arguments suffixed with |", +]; +DISPATCHABLE_COMMAND( seeargs ? 0 0 azArg nArg p ){ + int ia = 0; + for (ia=1; iaout, "%s%s", azArg[ia], (ia==nArg-1)? "|\n" : "|"); + return DCR_Ok; +} + +CONDITION_COMMAND(archive !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB)); +/***************** + * The .archive command + */ +COLLECT_HELP_TEXT[ + ".archive ... Manage SQL archives", + " Each command must have exactly one of the following options:", + " -c, --create Create a new archive", + " -u, --update Add or update files with changed mtime", + " -i, --insert Like -u but always add even if unchanged", + " -r, --remove Remove files from archive", + " -t, --list List contents of archive", + " -x, --extract Extract files from archive", + " Optional arguments:", + " -v, --verbose Print each filename as it is processed", + " -f FILE, --file FILE Use archive FILE (default is current db)", + " -a FILE, --append FILE Open FILE using the apndvfs VFS", + " -C DIR, --directory DIR Read/extract files from directory DIR", + " -g, --glob Use glob matching for names in archive", + " -n, --dryrun Show the SQL that would have occurred", + " Examples:", + " .ar -cf ARCHIVE foo bar # Create ARCHIVE from files foo and bar", + " .ar -tf ARCHIVE # List members of ARCHIVE", + " .ar -xvf ARCHIVE # Verbosely extract files from ARCHIVE", + " See also:", + " http://sqlite.org/cli.html#sqlite_archive_support", +]; +DISPATCHABLE_COMMAND( archive ? 0 0 azArg nArg p ){ + open_db(p, 0); + if( ISS(p)->bSafeMode ) return DCR_AbortError; + return arDotCommand(p, 0, azArg, nArg); +} + +/***************** + * The .auth command + */ +CONDITION_COMMAND(auth !defined(SQLITE_OMIT_AUTHORIZATION)); +COLLECT_HELP_TEXT[ + ".auth ON|OFF Show authorizer callbacks", +]; +DISPATCHABLE_COMMAND( auth 3 2 2 azArg nArg p ){ + open_db(p, 0); + if( booleanValue(azArg[1]) ){ + sqlite3_set_authorizer(DBX(p), shellAuth, p); + }else if( ISS(p)->bSafeModeFuture ){ + sqlite3_set_authorizer(DBX(p), safeModeAuth, p); + }else{ + sqlite3_set_authorizer(DBX(p), 0, 0); + } + return DCR_Ok; +} + +/***************** + * The .backup and .save commands (aliases for each other) + * These defer to writeDb in the dispatch table, so are not here. + */ +COLLECT_HELP_TEXT[ + ".backup ?DB? FILE Backup DB (default \"main\") to FILE", + " Options:", + " --append Use the appendvfs", + " --async Write the FILE without journal and fsync()", + ".save ?DB? FILE Write DB (default \"main\") to FILE", + " Options:", + " --append Use the appendvfs", + " --async Write the FILE without journal and fsync()", +]; +DISPATCHABLE_COMMAND( backup 4 2 5 ){ + return writeDb( azArg, nArg, p, pzErr); +} +DISPATCHABLE_COMMAND( save 3 2 5 ){ + return writeDb( azArg, nArg, p, pzErr); +} + +/***************** + * The .bail command + */ +COLLECT_HELP_TEXT[ + ".bail on|off Stop after hitting an error. Default OFF", +]; +DISPATCHABLE_COMMAND( bail 3 2 2 ){ + bail_on_error = booleanValue(azArg[1]); + return DCR_Ok; +} + +/***************** + * The .binary and .cd commands + */ +COLLECT_HELP_TEXT[ + ".binary on|off Turn binary output on or off. Default OFF", + ".cd DIRECTORY Change the working directory to DIRECTORY", +]; +DISPATCHABLE_COMMAND( binary 3 2 2 ){ + if( booleanValue(azArg[1]) ){ + setBinaryMode(ISS(p)->out, 1); + }else{ + setTextMode(ISS(p)->out, 1); + } + return DCR_Ok; +} + +DISPATCHABLE_COMMAND( cd ? 2 2 ){ + int rc=0; + if( ISS(p)->bSafeMode ) return DCR_AbortError; +#if defined(_WIN32) || defined(WIN32) + wchar_t *z = sqlite3_win32_utf8_to_unicode(azArg[1]); + rc = !SetCurrentDirectoryW(z); + sqlite3_free(z); +#else + rc = chdir(azArg[1]); +#endif + if( rc ){ + utf8_printf(STD_ERR, "Cannot change to directory \"%s\"\n", azArg[1]); + rc = 1; + } + return DCR_Ok|rc; +} + +/* The ".breakpoint" command causes a call to the no-op routine named + * test_breakpoint(). It is undocumented. +*/ +COLLECT_HELP_TEXT[ + ",breakpoint calls test_breakpoint(). (a debugging aid)", +]; +DISPATCHABLE_COMMAND( breakpoint 3 1 1 ){ + test_breakpoint(); + return DCR_Ok; +} + +/***************** + * The .changes, .check, .clone and .connection commands + */ +COLLECT_HELP_TEXT[ + ".changes on|off Show number of rows changed by SQL", + ",check GLOB Fail if output since .testcase does not match", + ".clone NEWDB Clone data into NEWDB from the existing database", + ".connection [close] [#] Open or close an auxiliary database connection", +]; +DISPATCHABLE_COMMAND( changes 3 2 2 ){ + setOrClearFlag(p, SHFLG_CountChanges, azArg[1]); + return DCR_Ok; +} +DISPATCHABLE_COMMAND( check 3 0 0 ){ + /* Cancel output redirection, if it is currently set (by .testcase) + ** Then read the content of the testcase-out.txt file and compare against + ** azArg[1]. If there are differences, report an error and exit. + */ + char *zRes = 0; + int rc = 0; + DotCmdRC rv = DCR_Ok; + output_reset(ISS(p)); + if( nArg!=2 ){ + return DCR_ArgWrong; + }else if( (zRes = readFile("testcase-out.txt", 0))==0 ){ + *pzErr = shellMPrintf(&rc, "Error: cannot read 'testcase-out.txt'"); + rv = DCR_Return; + }else if( testcase_glob(azArg[1],zRes)==0 ){ + *pzErr = + shellMPrintf(&rc, + "testcase-%s FAILED\n Expected: [%s]\n Got: [%s]\n", + ISS(p)->zTestcase, azArg[1], zRes); + rv = DCR_Error; + }else{ + utf8_printf(STD_OUT, "testcase-%s ok\n", ISS(p)->zTestcase); + ISS(p)->nCheck++; + } + sqlite3_free(zRes); + return (rc==SQLITE_NOMEM)? DCR_Abort : rv; +} +DISPATCHABLE_COMMAND( clone ? 2 2 ){ + if( ISS(p)->bSafeMode ) return DCR_AbortError; + tryToClone(p, azArg[1]); + return DCR_Ok; +} +DISPATCHABLE_COMMAND( connection ? 1 4 ){ + ShellInState *psi = ISS(p); + if( nArg==1 ){ + /* List available connections */ + int i; + for(i=0; iaAuxDb); i++){ + const char *zFile = psi->aAuxDb[i].zDbFilename; + if( psi->aAuxDb[i].db==0 && psi->pAuxDb!=&psi->aAuxDb[i] ){ + zFile = "(not open)"; + }else if( zFile==0 ){ + zFile = "(memory)"; + }else if( zFile[0]==0 ){ + zFile = "(temporary-file)"; + } + if( psi->pAuxDb == &psi->aAuxDb[i] ){ + utf8_printf(STD_OUT, "ACTIVE %d: %s\n", i, zFile); + }else if( psi->aAuxDb[i].db!=0 ){ + utf8_printf(STD_OUT, " %d: %s\n", i, zFile); + } + } + }else if( nArg==2 && IsDigit(azArg[1][0]) && azArg[1][1]==0 ){ + int i = azArg[1][0] - '0'; + if( psi->pAuxDb != &psi->aAuxDb[i] && i>=0 && iaAuxDb) ){ + psi->pAuxDb->db = DBI(psi); + psi->pAuxDb = &psi->aAuxDb[i]; +#if SHELL_DYNAMIC_EXTENSION + if( DBI(psi)!=0 ) notify_subscribers(psi, NK_DbUserVanishing, DBI(psi)); +#endif + globalDb = DBI(psi) = psi->pAuxDb->db; +#if SHELL_DYNAMIC_EXTENSION + if( DBI(psi)!=0 ) notify_subscribers(psi, NK_DbUserAppeared, DBI(psi)); +#endif + psi->pAuxDb->db = 0; + } + }else if( nArg==3 && strcmp(azArg[1], "close")==0 + && IsDigit(azArg[2][0]) && azArg[2][1]==0 ){ + int i = azArg[2][0] - '0'; + if( i<0 || i>=ArraySize(psi->aAuxDb) ){ + /* No-op */ + }else if( psi->pAuxDb == &psi->aAuxDb[i] ){ + raw_printf(STD_ERR, "cannot close the active database connection\n"); + return DCR_Error; + }else if( psi->aAuxDb[i].db ){ + session_close_all(psi, i); + close_db(psi->aAuxDb[i].db); + psi->aAuxDb[i].db = 0; + } + }else{ + return DCR_ArgWrong; + } + return DCR_Ok; +} + +/***************** + * The .databases, .dbconfig and .dbinfo commands + */ +COLLECT_HELP_TEXT[ + ".databases List names and files of attached databases", + ".dbconfig ?op? ?val? List or change sqlite3_db_config() options", + ".dbinfo ?DB? Show status information about the database", +]; +/* Allow garbage arguments on this, to be ignored. */ +DISPATCHABLE_COMMAND( databases 2 1 0 ){ + int rc; + char **azName = 0; + int nName = 0; + sqlite3_stmt *pStmt; + sqlite3 *db; int i; - int nOrphan = -1; - RecoverTable *pOrphan = 0; + open_db(p, 0); + db = DBX(p); + rc = sqlite3_prepare_v2(db, "PRAGMA database_list", -1, &pStmt, 0); + if( rc ){ + *pzErr = smprintf("%s\n", sqlite3_errmsg(db)); + rc = 1; + }else{ + while( sqlite3_step(pStmt)==SQLITE_ROW ){ + const char *zSchema = (const char *)sqlite3_column_text(pStmt,1); + const char *zFile = (const char*)sqlite3_column_text(pStmt,2); + if( zSchema==0 || zFile==0 ) continue; + azName = sqlite3_realloc(azName, (nName+1)*2*sizeof(char*)); + shell_check_oom(azName); + azName[nName*2] = strdup(zSchema); + shell_check_oom(azName[nName*2]); + azName[nName*2+1] = strdup(zFile); + shell_check_oom(azName[nName*2+1]); + nName++; + } + } + sqlite3_finalize(pStmt); + for(i=0; iout, "%s: %s %s%s\n", + azName[i*2], + z && z[0] ? z : "\"\"", + bRdonly ? "r/o" : "r/w", + eTxn==SQLITE_TXN_NONE ? "" : + eTxn==SQLITE_TXN_READ ? " read-txn" : " write-txn"); + free(azName[i*2]); + free(azName[i*2+1]); + } + sqlite3_free(azName); + return DCR_Ok|(rc!=0); +} +DISPATCHABLE_COMMAND( dbconfig 3 1 3 ){ + static const struct DbConfigChoices { + const char *zName; + int op; + } aDbConfig[] = { + { "defensive", SQLITE_DBCONFIG_DEFENSIVE }, + { "dqs_ddl", SQLITE_DBCONFIG_DQS_DDL }, + { "dqs_dml", SQLITE_DBCONFIG_DQS_DML }, + { "enable_fkey", SQLITE_DBCONFIG_ENABLE_FKEY }, + { "enable_qpsg", SQLITE_DBCONFIG_ENABLE_QPSG }, + { "enable_trigger", SQLITE_DBCONFIG_ENABLE_TRIGGER }, + { "enable_view", SQLITE_DBCONFIG_ENABLE_VIEW }, + { "fts3_tokenizer", SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER }, + { "legacy_alter_table", SQLITE_DBCONFIG_LEGACY_ALTER_TABLE }, + { "legacy_file_format", SQLITE_DBCONFIG_LEGACY_FILE_FORMAT }, + { "load_extension", SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION }, + { "no_ckpt_on_close", SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE }, + { "reset_database", SQLITE_DBCONFIG_RESET_DATABASE }, + { "trigger_eqp", SQLITE_DBCONFIG_TRIGGER_EQP }, + { "trusted_schema", SQLITE_DBCONFIG_TRUSTED_SCHEMA }, + { "writable_schema", SQLITE_DBCONFIG_WRITABLE_SCHEMA }, + }; + int ii, v; + open_db(p, 0); + for(ii=0; ii1 && strcmp(azArg[1], aDbConfig[ii].zName)!=0 ) continue; + if( nArg>=3 ){ + sqlite3_db_config(DBX(p), aDbConfig[ii].op, booleanValue(azArg[2]), 0); + } + sqlite3_db_config(DBX(p), aDbConfig[ii].op, -1, &v); + utf8_printf(ISS(p)->out, "%19s %s\n", + aDbConfig[ii].zName, v ? "on" : "off"); + if( nArg>1 ) break; + } + if( nArg>1 && ii==ArraySize(aDbConfig) ){ + *pzErr = smprintf("Error: unknown dbconfig \"%s\"\n" + "Enter \".dbconfig\" with no arguments for a list\n", + azArg[1]); + return DCR_ArgWrong; + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( dbinfo 3 1 2 ){ + return shell_dbinfo_command(p, nArg, azArg); +} + +/***************** + * The .dump, .echo and .eqp commands + */ +COLLECT_HELP_TEXT[ + ".dump ?OBJECTS? Render database content as SQL", + " Options:", + " --data-only Output only INSERT statements", + " --newlines Allow unescaped newline characters in output", + " --nosys Omit system tables (ex: \"sqlite_stat1\")", + " --preserve-rowids Include ROWID values in the output", + " --schema SCHEMA Dump table(s) from given SCHEMA", + " OBJECTS is a LIKE pattern for tables, indexes, triggers or views to dump", + " Additional LIKE patterns can be given in subsequent arguments", + ".echo on|off Turn command echo on or off", + ".eqp on|off|full|... Enable or disable automatic EXPLAIN QUERY PLAN", + " Other Modes:", +#ifdef SQLITE_DEBUG + " test Show raw EXPLAIN QUERY PLAN output", + " trace Like \"full\" but enable \"PRAGMA vdbe_trace\"", +#endif + " trigger Like \"full\" but also show trigger bytecode", +]; +DISPATCHABLE_COMMAND( dump ? 1 2 ){ + ShellInState *psi = ISS(p); + char *zLike = 0; + char *zSchema = "main"; + char *zSql; + int i; + int savedShowHeader = psi->showHeader; + int savedShellFlags = psi->shellFlgs; + ShellClearFlag(p, + SHFLG_PreserveRowid|SHFLG_Newlines|SHFLG_Echo + |SHFLG_DumpDataOnly|SHFLG_DumpNoSys); + for(i=1; ishellFlgs & SHFLG_DumpDataOnly)==0 ){ + /* When playing back a "dump", the content might appear in an order + ** which causes immediate foreign key constraints to be violated. + ** So disable foreign-key constraint enforcement to prevent problems. */ + raw_printf(psi->out, "PRAGMA foreign_keys=OFF;\n"); + raw_printf(psi->out, "BEGIN TRANSACTION;\n"); + } + psi->writableSchema = 0; + psi->showHeader = 0; + /* Set writable_schema=ON since doing so forces SQLite to initialize + ** as much of the schema as it can even if the sqlite_schema table is + ** corrupt. */ + sqlite3_exec(DBX(p), "SAVEPOINT dump; PRAGMA writable_schema=ON", 0, 0, 0); + psi->nErr = 0; + if( zLike==0 ) zLike = smprintf("true"); + zSql = smprintf("SELECT name, type, sql FROM %w.sqlite_schema AS o " + "WHERE (%s) AND type=='table' AND sql NOT NULL" + " ORDER BY tbl_name='sqlite_sequence', rowid", + zSchema, zLike); + run_schema_dump_query(psi,zSql); + sqlite3_free(zSql); + if( (psi->shellFlgs & SHFLG_DumpDataOnly)==0 ){ + zSql = smprintf( + "SELECT sql FROM sqlite_schema AS o " + "WHERE (%s) AND sql NOT NULL" + " AND type IN ('index','trigger','view')", + zLike + ); + run_table_dump_query(psi, zSql); + sqlite3_free(zSql); + } + sqlite3_free(zLike); + if( psi->writableSchema ){ + raw_printf(psi->out, "PRAGMA writable_schema=OFF;\n"); + psi->writableSchema = 0; + } + sqlite3_exec(DBX(p), "PRAGMA writable_schema=OFF;", 0, 0, 0); + sqlite3_exec(DBX(p), "RELEASE dump;", 0, 0, 0); + if( (psi->shellFlgs & SHFLG_DumpDataOnly)==0 ){ + raw_printf(psi->out, psi->nErr?"ROLLBACK; -- due to errors\n":"COMMIT;\n"); + } + psi->showHeader = savedShowHeader; + psi->shellFlgs = savedShellFlags; + + return DCR_Ok; +} +DISPATCHABLE_COMMAND( echo ? 2 2 ){ + setOrClearFlag(p, SHFLG_Echo, azArg[1]); + return DCR_Ok; +} +DISPATCHABLE_COMMAND( eqp ? 0 0 ){ + ShellInState *psi = ISS(p); + if( nArg==2 ){ + psi->autoEQPtest = 0; + if( psi->autoEQPtrace ){ + if( DBX(p) ) sqlite3_exec(DBX(p), "PRAGMA vdbe_trace=OFF;", 0, 0, 0); + psi->autoEQPtrace = 0; + } + if( strcmp(azArg[1],"full")==0 ){ + psi->autoEQP = AUTOEQP_full; + }else if( strcmp(azArg[1],"trigger")==0 ){ + psi->autoEQP = AUTOEQP_trigger; +#ifdef SQLITE_DEBUG + }else if( strcmp(azArg[1],"test")==0 ){ + psi->autoEQP = AUTOEQP_on; + psi->autoEQPtest = 1; + }else if( strcmp(azArg[1],"trace")==0 ){ + psi->autoEQP = AUTOEQP_full; + psi->autoEQPtrace = 1; + open_db(p, 0); + sqlite3_exec(DBX(p), "SELECT name FROM sqlite_schema LIMIT 1", 0, 0, 0); + sqlite3_exec(DBX(p), "PRAGMA vdbe_trace=ON;", 0, 0, 0); +#endif + }else{ + psi->autoEQP = (u8)booleanValue(azArg[1]); + } + }else{ + return DCR_ArgWrong; + } + return DCR_Ok; +} + +/***************** + * The .cease, .exit and .quit commands + * These are together so that their differing effects are apparent. + */ +CONDITION_COMMAND(cease defined(SHELL_CEASE)); +COLLECT_HELP_TEXT[ + ".cease ?CODE? Cease shell operation, with optional return code", + " Return code defaults to 0, otherwise is limited to non-signal values", + ".exit ?CODE? Exit shell program, maybe with return-code CODE", + " Exit immediately if CODE != 0, else functions as \"quit this input\"", + ".quit Quit processing this input or script", +]; +DISPATCHABLE_COMMAND( cease 4 1 2 ){ + /* .cease effects an exit, always. Only the exit code is variable. */ + int rc = 0; + if( nArg>1 ){ + rc = (int)integerValue(azArg[1]); + if( rc>0x7f ) rc = 0x7f; + } + p->shellAbruptExit = 0x100|rc; + return DCR_Exit; +} +DISPATCHABLE_COMMAND( exit 3 1 0 ){ + /* .exit acts like .quit with no argument or a zero argument, + * only returning. With a non-zero argument, it effects an exit. */ + int rc; + if( nArg>1 && (rc = (int)integerValue(azArg[1]))!=0 ){ + rc &= 0xff; /* Mimic effect of legacy call to exit(). */ + p->shellAbruptExit = 0x100|rc; + } + return DCR_Return; +} +DISPATCHABLE_COMMAND( quit 1 1 0 ){ + /* .quit would be more aptly named .return, as it does nothing more. */ + return DCR_Return; +} + +/***************** + * The .expert and .explain commands + */ +CONDITION_COMMAND( expert !defined(SQLITE_OMIT_VIRTUALTABLE) ); +COLLECT_HELP_TEXT[ + ".expert Suggest indexes for queries", + ".explain ?on|off|auto? Change the EXPLAIN formatting mode. Default: auto", +]; +DISPATCHABLE_COMMAND( expert ? 1 1 ){ + ShellInState *psi = ISS(p); + int rv = DCR_Ok; + char *zErr = 0; + int i; + int iSample = 0; + + if( psi->bSafeMode ) return DCR_AbortError; + assert( psi->expert.pExpert==0 ); + memset(&psi->expert, 0, sizeof(ExpertInfo)); + + open_db(p, 0); - int bFreelist = 1; /* 0 if --freelist-corrupt is specified */ - int bRowids = 1; /* 0 if --no-rowids */ for(i=1; i=2 && 0==strncmp(z, "-verbose", n) ){ + psi->expert.bVerbose = 1; + } + else if( n>=2 && 0==strncmp(z, "-sample", n) ){ + if( i==(nArg-1) ){ + return DCR_Unpaired|i; + }else{ + iSample = (int)integerValue(azArg[++i]); + if( iSample<0 || iSample>100 ){ + *pzErr = smprintf("value out of range: %s\n", azArg[i]); + return DCR_ArgWrong|i; + } + } + } + else{ + return DCR_Unknown|i; + } + } + + psi->expert.pExpert = sqlite3_expert_new(DBI(psi), &zErr); + if( psi->expert.pExpert==0 ){ + *pzErr = smprintf("sqlite3_expert_new: %s\n", + zErr ? zErr : "out of memory"); + return DCR_Error; + }else{ + sqlite3_expert_config(psi->expert.pExpert, EXPERT_CONFIG_SAMPLE, iSample); + } + + return DCR_Ok; +} + +DISPATCHABLE_COMMAND( explain ? 1 2 ){ + /* The ".explain" command is automatic now. It is largely + ** pointless, retained purely for backwards compatibility */ + ShellInState *psi = ISS(p); + int val = 1; + if( nArg>1 ){ + if( strcmp(azArg[1],"auto")==0 ){ + val = 99; + }else{ + val = booleanValue(azArg[1]); + } + } + if( val==1 && psi->mode!=MODE_Explain ){ + psi->normalMode = psi->mode; + psi->mode = MODE_Explain; + psi->autoExplain = 0; + }else if( val==0 ){ + if( psi->mode==MODE_Explain ) psi->mode = psi->normalMode; + psi->autoExplain = 0; + }else if( val==99 ){ + if( psi->mode==MODE_Explain ) psi->mode = psi->normalMode; + psi->autoExplain = 1; + } + return DCR_Ok; +} + +/***************** + * The .excel, .once and .output commands + * These share much implementation, so they stick together. + */ +COLLECT_HELP_TEXT[ + ".excel Display the output of next command in spreadsheet", + " --bom Prefix the file with a UTF8 byte-order mark", + ".once ?OPTIONS? ?FILE? Output for the next SQL command only to FILE", + " If FILE begins with '|' then open it as a command to be piped into.", + " Options:", + " --bom Prefix output with a UTF8 byte-order mark", + " -e Send output to the system text editor", + " -x Send output as CSV to a spreadsheet (same as \".excel\")", + ".output ?FILE? Send output to FILE or stdout if FILE is omitted", + " If FILE begins with '|' then open it as a command to be piped into.", + " Options:", + " --bom Prefix output with a UTF8 byte-order mark", + " -e Send output to the system text editor", + " -x Send output as CSV to a spreadsheet (same as \".excel\")", +]; +/* Shared implementation of .excel, .once and .output */ +static DotCmdRC outputRedirs(char *azArg[], int nArg, + ShellInState *psi, char **pzErr, + int bOnce, int eMode){ + /* bOnce => 0: .output, 1: .once, 2: .excel */ + /* eMode => 'x' for excel, else 0 */ + int rc = 0; + char *zFile = 0; + u8 bTxtMode = 0; + u8 bPutBOM = 0; + int i; + static unsigned const char zBOM[4] = {0xef,0xbb,0xbf,0}; + if( psi->bSafeMode ) return DCR_AbortError; + for(i=1; ioutCount = 2; + }else{ + psi->outCount = 0; + } + output_reset(psi); +#ifndef SQLITE_NOHAVE_SYSTEM + if( eMode=='e' || eMode=='x' ){ + psi->doXdgOpen = 1; + outputModePush(psi); + if( eMode=='x' ){ + /* spreadsheet mode. Output as CSV. */ + newTempFile(psi, "csv"); + psi->shellFlgs &= ~SHFLG_Echo; + psi->mode = MODE_Csv; + sqlite3_snprintf(sizeof(psi->colSeparator), psi->colSeparator, SEP_Comma); + sqlite3_snprintf(sizeof(psi->rowSeparator), psi->rowSeparator, SEP_CrLf); + }else{ + /* text editor mode */ + newTempFile(psi, "txt"); + bTxtMode = 1; + } + sqlite3_free(zFile); + zFile = smprintf("%s", psi->zTempFile); + } +#endif /* SQLITE_NOHAVE_SYSTEM */ + shell_check_oom(zFile); + if( zFile[0]=='|' ){ +#ifdef SQLITE_OMIT_POPEN + *pzErr = smprintf("pipes are not supported in this OS\n"); + rc = 1; + psi->out = STD_OUT; +#else + psi->out = popen(zFile + 1, "w"); + if( psi->out==0 ){ + *pzErr = smprintf("cannot open pipe \"%s\"\n", zFile + 1); + psi->out = STD_OUT; + rc = 1; + }else{ + if( bPutBOM ) fwrite(zBOM, 1, 3, psi->out); + sqlite3_snprintf(sizeof(psi->outfile), psi->outfile, "%s", zFile); + } +#endif + }else{ + psi->out = output_file_open(zFile, bTxtMode); + if( psi->out==0 ){ + if( strcmp(zFile,"off")!=0 ){ + *pzErr = smprintf("cannot write to \"%s\"\n", zFile); + } + psi->out = STD_OUT; + rc = 1; + } else { + if( bPutBOM ) fwrite(zBOM, 1, 3, psi->out); + sqlite3_snprintf(sizeof(psi->outfile), psi->outfile, "%s", zFile); + } + } + sqlite3_free(zFile); + return DCR_Ok|rc; +} +DISPATCHABLE_COMMAND( excel ? 1 2 ){ + return outputRedirs(azArg, nArg, ISS(p), pzErr, 2, 'x'); +} +DISPATCHABLE_COMMAND( once ? 1 6 ){ + return outputRedirs(azArg, nArg, ISS(p), pzErr, 1, 0); +} +DISPATCHABLE_COMMAND( output ? 1 6 ){ + return outputRedirs(azArg, nArg, ISS(p), pzErr, 0, 0); +} + + +/***************** + * The .filectrl and fullschema commands + */ +COLLECT_HELP_TEXT[ + ".filectrl CMD ... Run various sqlite3_file_control() operations", + " --schema SCHEMA Use SCHEMA instead of \"main\"", + " --help Show CMD details", + ".fullschema ?--indent? Show schema and the content of sqlite_stat tables", +]; +DISPATCHABLE_COMMAND( filectrl ? 2 0 ){ + static const struct { + const char *zCtrlName; /* Name of a test-control option */ + int ctrlCode; /* Integer code for that option */ + const char *zUsage; /* Usage notes */ + } aCtrl[] = { + { "chunk_size", SQLITE_FCNTL_CHUNK_SIZE, "SIZE" }, + { "data_version", SQLITE_FCNTL_DATA_VERSION, "" }, + { "has_moved", SQLITE_FCNTL_HAS_MOVED, "" }, + { "lock_timeout", SQLITE_FCNTL_LOCK_TIMEOUT, "MILLISEC" }, + { "persist_wal", SQLITE_FCNTL_PERSIST_WAL, "[BOOLEAN]" }, + /* { "pragma", SQLITE_FCNTL_PRAGMA, "NAME ARG" },*/ + { "psow", SQLITE_FCNTL_POWERSAFE_OVERWRITE, "[BOOLEAN]" }, + { "reserve_bytes", SQLITE_FCNTL_RESERVE_BYTES, "[N]" }, + { "size_limit", SQLITE_FCNTL_SIZE_LIMIT, "[LIMIT]" }, + { "tempfilename", SQLITE_FCNTL_TEMPFILENAME, "" }, + /* { "win32_av_retry", SQLITE_FCNTL_WIN32_AV_RETRY, "COUNT DELAY" },*/ + }; + ShellInState *psi = ISS(p); + int filectrl = -1; + int iCtrl = -1; + sqlite3_int64 iRes = 0; /* Integer result to display if rc2==1 */ + int isOk = 0; /* 0: usage 1: %lld 2: no-result */ + int n2, i; + const char *zCmd = 0; + const char *zSchema = 0; + + open_db(p, 0); + zCmd = nArg>=2 ? azArg[1] : "help"; + + if( zCmd[0]=='-' + && (strcmp(zCmd,"--schema")==0 || strcmp(zCmd,"-schema")==0) + && nArg>=4 + ){ + zSchema = azArg[2]; + for(i=3; iout, "Available file-controls:\n"); + for(i=0; iout, " .filectrl %s %s\n", + aCtrl[i].zCtrlName, aCtrl[i].zUsage); + } + return DCR_Error; + } + + /* Convert filectrl text option to value. Allow any + ** unique prefix of the option name, or a numerical value. */ + n2 = strlen30(zCmd); + for(i=0; iout, azArg[0]); - return 1; + case SQLITE_FCNTL_LOCK_TIMEOUT: + case SQLITE_FCNTL_CHUNK_SIZE: { + int x; + if( nArg!=3 ) break; + x = (int)integerValue(azArg[2]); + sqlite3_file_control(DBX(p), zSchema, filectrl, &x); + isOk = 2; + break; + } + case SQLITE_FCNTL_PERSIST_WAL: + case SQLITE_FCNTL_POWERSAFE_OVERWRITE: { + int x; + if( nArg!=2 && nArg!=3 ) break; + x = nArg==3 ? booleanValue(azArg[2]) : -1; + sqlite3_file_control(DBX(p), zSchema, filectrl, &x); + iRes = x; + isOk = 1; + break; + } + case SQLITE_FCNTL_DATA_VERSION: + case SQLITE_FCNTL_HAS_MOVED: { + int x; + if( nArg!=2 ) break; + sqlite3_file_control(DBX(p), zSchema, filectrl, &x); + iRes = x; + isOk = 1; + break; } + case SQLITE_FCNTL_TEMPFILENAME: { + char *z = 0; + if( nArg!=2 ) break; + sqlite3_file_control(DBX(p), zSchema, filectrl, &z); + if( z ){ + utf8_printf(psi->out, "%s\n", z); + sqlite3_free(z); + } + isOk = 2; + break; + } + case SQLITE_FCNTL_RESERVE_BYTES: { + int x; + if( nArg>=3 ){ + x = atoi(azArg[2]); + sqlite3_file_control(DBX(p), zSchema, filectrl, &x); + } + x = -1; + sqlite3_file_control(DBX(p), zSchema, filectrl, &x); + utf8_printf(psi->out,"%d\n", x); + isOk = 2; + break; + } + } + } + if( isOk==0 && iCtrl>=0 ){ + utf8_printf(psi->out, "Usage: .filectrl %s %s\n", zCmd,aCtrl[iCtrl].zUsage); + return DCR_CmdErred; + }else if( isOk==1 ){ + char zBuf[100]; + sqlite3_snprintf(sizeof(zBuf), zBuf, "%lld", iRes); + raw_printf(psi->out, "%s\n", zBuf); } + return DCR_Ok; +} - shellExecPrintf(pState->db, &rc, - /* Attach an in-memory database named 'recovery'. Create an indexed - ** cache of the sqlite_dbptr virtual table. */ - "PRAGMA writable_schema = on;" - "ATTACH %Q AS recovery;" - "DROP TABLE IF EXISTS recovery.dbptr;" - "DROP TABLE IF EXISTS recovery.freelist;" - "DROP TABLE IF EXISTS recovery.map;" - "DROP TABLE IF EXISTS recovery.schema;" - "CREATE TABLE recovery.freelist(pgno INTEGER PRIMARY KEY);", zRecoveryDb +DISPATCHABLE_COMMAND( fullschema ? 1 2 ){ + int rc; + ShellInState data; + ShellExState datax; + int doStats = 0; + /* Consider some refactoring to avoid this wholesale copying. */ + memcpy(&data, ISS(p), sizeof(data)); + memcpy(&datax, p, sizeof(datax)); + data.pSXS = &datax; + datax.pSIS = &data; + data.showHeader = 0; + data.cMode = data.mode = MODE_Semi; + if( nArg==2 && optionMatch(azArg[1], "indent") ){ + data.cMode = data.mode = MODE_Pretty; + nArg = 1; + } + if( nArg!=1 ){ + return DCR_TooMany|1; + } + open_db(p, 0); + rc = sqlite3_exec(datax.dbUser, + "SELECT sql FROM" + " (SELECT sql sql, type type, tbl_name tbl_name, name name, rowid x" + " FROM sqlite_schema UNION ALL" + " SELECT sql, type, tbl_name, name, rowid FROM sqlite_temp_schema) " + "WHERE type!='meta' AND sql NOTNULL AND name NOT LIKE 'sqlite_%' " + "ORDER BY x", + callback, &datax, 0 ); - - if( bFreelist ){ - shellExec(pState->db, &rc, - "WITH trunk(pgno) AS (" - " SELECT shell_int32(" - " (SELECT data FROM sqlite_dbpage WHERE pgno=1), 8) AS x " - " WHERE x>0" - " UNION" - " SELECT shell_int32(" - " (SELECT data FROM sqlite_dbpage WHERE pgno=trunk.pgno), 0) AS x " - " FROM trunk WHERE x>0" - ")," - "freelist(data, n, freepgno) AS (" - " SELECT data, min(16384, shell_int32(data, 1)-1), t.pgno " - " FROM trunk t, sqlite_dbpage s WHERE s.pgno=t.pgno" - " UNION ALL" - " SELECT data, n-1, shell_int32(data, 2+n) " - " FROM freelist WHERE n>=0" - ")" - "REPLACE INTO recovery.freelist SELECT freepgno FROM freelist;" - ); + if( rc==SQLITE_OK ){ + sqlite3_stmt *pStmt; + rc = sqlite3_prepare_v2(datax.dbUser, + "SELECT rowid FROM sqlite_schema" + " WHERE name GLOB 'sqlite_stat[134]'", + -1, &pStmt, 0); + doStats = sqlite3_step(pStmt)==SQLITE_ROW; + sqlite3_finalize(pStmt); + } + if( doStats==0 ){ + raw_printf(data.out, "/* No STAT tables available */\n"); + }else{ + raw_printf(data.out, "ANALYZE sqlite_schema;\n"); + data.cMode = data.mode = MODE_Insert; + datax.zDestTable = "sqlite_stat1"; + shell_exec(&datax, "SELECT * FROM sqlite_stat1", 0); + datax.zDestTable = "sqlite_stat4"; + shell_exec(&datax, "SELECT * FROM sqlite_stat4", 0); + raw_printf(data.out, "ANALYZE sqlite_schema;\n"); } + return rc > 0; +} - /* If this is an auto-vacuum database, add all pointer-map pages to - ** the freelist table. Do this regardless of whether or not - ** --freelist-corrupt was specified. */ - shellExec(pState->db, &rc, - "WITH ptrmap(pgno) AS (" - " SELECT 2 WHERE shell_int32(" - " (SELECT data FROM sqlite_dbpage WHERE pgno=1), 13" - " )" - " UNION ALL " - " SELECT pgno+1+(SELECT page_size FROM pragma_page_size)/5 AS pp " - " FROM ptrmap WHERE pp<=(SELECT page_count FROM pragma_page_count)" - ")" - "REPLACE INTO recovery.freelist SELECT pgno FROM ptrmap" - ); +/***************** + * The .headers command + */ +COLLECT_HELP_TEXT[ + ".headers on|off Turn display of headers on or off", +]; +DISPATCHABLE_COMMAND( headers 6 2 2 ){ + ISS(p)->showHeader = booleanValue(azArg[1]); + ISS(p)->shellFlgs |= SHFLG_HeaderSet; + return DCR_Ok; +} - shellExec(pState->db, &rc, - "CREATE TABLE recovery.dbptr(" - " pgno, child, PRIMARY KEY(child, pgno)" - ") WITHOUT ROWID;" - "INSERT OR IGNORE INTO recovery.dbptr(pgno, child) " - " SELECT * FROM sqlite_dbptr" - " WHERE pgno NOT IN freelist AND child NOT IN freelist;" +/***************** + * The .help command + */ - /* Delete any pointer to page 1. This ensures that page 1 is considered - ** a root page, regardless of how corrupt the db is. */ - "DELETE FROM recovery.dbptr WHERE child = 1;" +/* This literal's value AND address are used for help's workings. */ +static const char *zHelpAll = "-all"; + +COLLECT_HELP_TEXT[ + ".help ?PATTERN?|?-all? Show help for PATTERN or everything, or summarize", + " Repeat -all to see undocumented commands", +]; +DISPATCHABLE_COMMAND( help 3 1 3 ){ + const char *zPat = 0; + FILE *out = ISS(p)->out; + if( nArg>1 ){ + char *z = azArg[1]; + if( nArg==3 && strcmp(z, zHelpAll)==0 && strcmp(azArg[2], zHelpAll)==0 ){ + /* Show the undocumented command help */ + zPat = zHelpAll; + }else if( strcmp(z,"-a")==0 || optionMatch(z, "all") ){ + zPat = ""; + }else{ + zPat = z; + } + } + if( showHelp(out, zPat, p)==0 && nArg>1 ){ + utf8_printf(out, "Nothing matches '%s'\n", azArg[1]); + } + /* Help pleas never fail! */ + return DCR_Ok; +} - /* Delete all pointers to any pages that have more than one pointer - ** to them. Such pages will be treated as root pages when recovering - ** data. */ - "DELETE FROM recovery.dbptr WHERE child IN (" - " SELECT child FROM recovery.dbptr GROUP BY child HAVING count(*)>1" - ");" +/***************** + * The .import command + */ +COLLECT_HELP_TEXT[ + ".import FILE TABLE Import data from FILE into TABLE", + " Options:", + " --ascii Use \\037 and \\036 as column and row separators", + " --csv Use , and \\n as column and row separators", + " --skip N Skip the first N rows of input", + " --schema S Target table to be S.TABLE", + " -v \"Verbose\" - increase auxiliary output", + " Notes:", + " * If TABLE does not exist, it is created. The first row of input", + " determines the column names.", + " * If neither --csv or --ascii are used, the input mode is derived", + " from the \".mode\" output mode", + " * If FILE begins with \"|\" then it is a command that generates the", + " input text.", +]; +DISPATCHABLE_COMMAND( import ? 3 7 ){ + char *zTable = 0; /* Insert data into this table */ + char *zSchema = 0; /* within this schema (may default to "main") */ + char *zFile = 0; /* Name of file to extra content from */ + sqlite3_stmt *pStmt = NULL; /* A statement */ + int nCol; /* Number of columns in the table */ + int nByte; /* Number of bytes in an SQL string */ + int i, j; /* Loop counters */ + int needCommit; /* True to COMMIT or ROLLBACK at end */ + int nSep; /* Number of bytes in psi->colSeparator[] */ + char *zSql; /* An SQL statement */ + char *zFullTabName; /* Table name with schema if applicable */ + ImportCtx sCtx; /* Reader context */ + char *(SQLITE_CDECL *xRead)(ImportCtx*); /* Func to read one value */ + int eVerbose = 0; /* Larger for more console output */ + int nSkip = 0; /* Initial lines to skip */ + int useOutputMode = 1; /* Use output mode to determine separators */ + FILE *out = ISS(p)->out; /* output stream */ + char *zCreate = 0; /* CREATE TABLE statement text */ + ShellInState *psi = ISS(p); + int rc = 0; - /* Create the "map" table that will (eventually) contain instructions - ** for dealing with each page in the db that contains one or more - ** records. */ - "CREATE TABLE recovery.map(" - "pgno INTEGER PRIMARY KEY, maxlen INT, intkey, root INT" - ");" + if(psi->bSafeMode) return DCR_AbortError; + memset(&sCtx, 0, sizeof(sCtx)); + if( 0==(sCtx.z = sqlite3_malloc64(120)) ){ + shell_out_of_memory(); + } - /* Populate table [map]. If there are circular loops of pages in the - ** database, the following adds all pages in such a loop to the map - ** as individual root pages. This could be handled better. */ - "WITH pages(i, maxlen) AS (" - " SELECT page_count, (" - " SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=page_count" - " ) FROM pragma_page_count WHERE page_count>0" - " UNION ALL" - " SELECT i-1, (" - " SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=i-1" - " ) FROM pages WHERE i>=2" - ")" - "INSERT INTO recovery.map(pgno, maxlen, intkey, root) " - " SELECT i, maxlen, NULL, (" - " WITH p(orig, pgno, parent) AS (" - " SELECT 0, i, (SELECT pgno FROM recovery.dbptr WHERE child=i)" - " UNION " - " SELECT i, p.parent, " - " (SELECT pgno FROM recovery.dbptr WHERE child=p.parent) FROM p" - " )" - " SELECT pgno FROM p WHERE (parent IS NULL OR pgno = orig)" - ") " - "FROM pages WHERE maxlen IS NOT NULL AND i NOT IN freelist;" - "UPDATE recovery.map AS o SET intkey = (" - " SELECT substr(data, 1, 1)==X'0D' FROM sqlite_dbpage WHERE pgno=o.pgno" - ");" + if( psi->mode==MODE_Ascii ){ + xRead = ascii_read_one_field; + }else{ + xRead = csv_read_one_field; + } + for(i=1; icolSeparator); + if( nSep==0 ){ + zYap = "non-null column separator required for import"; + } + if( nSep>1 ){ + zYap = "multi-character or multi-byte column separators" + " not allowed for import"; + } + nSep = strlen30(psi->rowSeparator); + if( nSep==0 ){ + zYap = "non-null row separator required for import"; + } + if( zYap!=0 ){ + *pzErr = smprintf("%s\n", zYap); + return DCR_Error; + } + if( nSep==2 && psi->mode==MODE_Csv + && strcmp(psi->rowSeparator,SEP_CrLf)==0 ){ + /* When importing CSV (only), if the row separator is set to the + ** default output row separator, change it to the default input + ** row separator. This avoids having to maintain different input + ** and output row separators. */ + sqlite3_snprintf(sizeof(psi->rowSeparator), psi->rowSeparator, SEP_Row); + nSep = strlen30(psi->rowSeparator); + } + if( nSep>1 ){ + *pzErr + = smprintf("multi-character row separators not allowed for import\n"); + return DCR_Error; + } + sCtx.cColSep = psi->colSeparator[0]; + sCtx.cRowSep = psi->rowSeparator[0]; + } + sCtx.zFile = zFile; + sCtx.nLine = 1; + if( sCtx.zFile[0]=='|' ){ +#ifdef SQLITE_OMIT_POPEN + *pzErr = smprintf("pipes are not supported in this OS\n"); + return DCR_Error; +#else + sCtx.in = popen(sCtx.zFile+1, "r"); + sCtx.zFile = ""; + sCtx.xCloser = pclose; +#endif + }else{ + sCtx.in = fopen(sCtx.zFile, "rb"); + sCtx.xCloser = fclose; + } + if( sCtx.in==0 ){ + *pzErr = smprintf("cannot open \"%s\"\n", zFile); + import_cleanup(&sCtx); + return DCR_Error; + } + /* Here and below, resources must be freed before exit. */ + if( eVerbose>=2 || (eVerbose>=1 && useOutputMode) ){ + char zSep[2]; + zSep[1] = 0; + zSep[0] = sCtx.cColSep; + utf8_printf(out, "Column separator "); + output_c_string(out, zSep); + utf8_printf(out, ", row separator "); + zSep[0] = sCtx.cRowSep; + output_c_string(out, zSep); + utf8_printf(out, "\n"); + } + while( (nSkip--)>0 ){ + while( xRead(&sCtx) && sCtx.cTerm==sCtx.cColSep ){} + } + if( zSchema!=0 ){ + zFullTabName = smprintf("\"%w\".\"%w\"", zSchema, zTable); + }else{ + zFullTabName = smprintf("\"%w\"", zTable); + } + zSql = smprintf("SELECT * FROM %s", zFullTabName); + if( zSql==0 || zFullTabName==0 ){ + import_cleanup(&sCtx); + shell_out_of_memory(); + } + nByte = strlen30(zSql); + rc = sqlite3_prepare_v2(DBX(p), zSql, -1, &pStmt, 0); + import_append_char(&sCtx, 0); /* To ensure sCtx.z is allocated */ + if( rc && sqlite3_strglob("no such table: *", sqlite3_errmsg(DBX(p)))==0 ){ + zCreate = smprintf("CREATE TABLE %s", zFullTabName); + sqlite3 *dbCols = 0; + char *zRenames = 0; + char *zColDefs; + while( xRead(&sCtx) ){ + zAutoColumn(sCtx.z, &dbCols, 0); + if( sCtx.cTerm!=sCtx.cColSep ) break; + } + zColDefs = zAutoColumn(0, &dbCols, &zRenames); + if( zRenames!=0 ){ + FILE *fh = INSOURCE_IS_INTERACTIVE(psi->pInSource)? out : STD_ERR; + utf8_printf(fh, "Columns renamed during .import %s due to duplicates:\n" + "%s\n", sCtx.zFile, zRenames); + sqlite3_free(zRenames); + } + assert(dbCols==0); + if( zColDefs==0 ){ + *pzErr = smprintf("%s: empty file\n", sCtx.zFile); + sqlite3_free(zCreate); + import_fail: /* entry from outer blocks */ + sqlite3_free(zSql); + sqlite3_free(zFullTabName); + import_cleanup(&sCtx); + return DCR_Error; + } + zCreate = smprintf("%z%z\n", zCreate, zColDefs); + if( eVerbose>=1 ){ + utf8_printf(out, "%s\n", zCreate); + } + rc = sqlite3_exec(DBX(p), zCreate, 0, 0, 0); + sqlite3_free(zCreate); + if( rc ){ + *pzErr = smprintf("%s failed:\n%s\n", zCreate, sqlite3_errmsg(DBX(p))); + goto import_fail; + } + rc = sqlite3_prepare_v2(DBX(p), zSql, -1, &pStmt, 0); + } + if( rc ){ + if (pStmt) sqlite3_finalize(pStmt); + *pzErr = smprintf("%s\n", sqlite3_errmsg(DBX(p))); + goto import_fail; + } + sqlite3_free(zSql); + nCol = sqlite3_column_count(pStmt); + sqlite3_finalize(pStmt); + pStmt = 0; + if( nCol==0 ) return DCR_Ok; /* no columns, no error */ + zSql = sqlite3_malloc64( nByte*2 + 20 + nCol*2 ); + if( zSql==0 ){ + import_cleanup(&sCtx); + shell_out_of_memory(); + } + sqlite3_snprintf(nByte+20, zSql, "INSERT INTO %s VALUES(?", zFullTabName); + j = strlen30(zSql); + for(i=1; i=2 ){ + utf8_printf(psi->out, "Insert using: %s\n", zSql); + } + rc = sqlite3_prepare_v2(DBX(p), zSql, -1, &pStmt, 0); + if( rc ){ + *pzErr = smprintf("%s\n", sqlite3_errmsg(DBX(p))); + if (pStmt) sqlite3_finalize(pStmt); + goto import_fail; + } + sqlite3_free(zSql); + sqlite3_free(zFullTabName); + needCommit = sqlite3_get_autocommit(DBX(p)); + if( needCommit ) sqlite3_exec(DBX(p), "BEGIN", 0, 0, 0); + do{ + int startLine = sCtx.nLine; + for(i=0; imode==MODE_Ascii && (z==0 || z[0]==0) && i==0 ) break; + sqlite3_bind_text(pStmt, i+1, z, -1, SQLITE_TRANSIENT); + if( i=nCol ){ + sqlite3_step(pStmt); + rc = sqlite3_reset(pStmt); + if( rc!=SQLITE_OK ){ + utf8_printf(STD_ERR, "%s:%d: INSERT failed: %s\n", sCtx.zFile, + startLine, sqlite3_errmsg(DBX(p))); + sCtx.nErr++; + }else{ + sCtx.nRow++; + } + } + }while( sCtx.cTerm!=EOF ); - /* Extract data from page 1 and any linked pages into table - ** recovery.schema. With the same schema as an sqlite_schema table. */ - "CREATE TABLE recovery.schema(type, name, tbl_name, rootpage, sql);" - "INSERT INTO recovery.schema SELECT " - " max(CASE WHEN field=0 THEN value ELSE NULL END)," - " max(CASE WHEN field=1 THEN value ELSE NULL END)," - " max(CASE WHEN field=2 THEN value ELSE NULL END)," - " max(CASE WHEN field=3 THEN value ELSE NULL END)," - " max(CASE WHEN field=4 THEN value ELSE NULL END)" - "FROM sqlite_dbdata WHERE pgno IN (" - " SELECT pgno FROM recovery.map WHERE root=1" - ")" - "GROUP BY pgno, cell;" - "CREATE INDEX recovery.schema_rootpage ON schema(rootpage);" - ); + import_cleanup(&sCtx); + sqlite3_finalize(pStmt); + if( needCommit ) sqlite3_exec(DBX(p), "COMMIT", 0, 0, 0); + if( eVerbose>0 ){ + utf8_printf(out, + "Added %d rows with %d errors using %d lines of input\n", + sCtx.nRow, sCtx.nErr, sCtx.nLine-1); + } + return DCR_Ok|(sCtx.nErr>0); +} - /* Open a transaction, then print out all non-virtual, non-"sqlite_%" - ** CREATE TABLE statements that extracted from the existing schema. */ +/***************** + * The .keyword command + */ +CONDITION_COMMAND( keyword !defined(NO_KEYWORD_COMMAND) ); +COLLECT_HELP_TEXT[ + ".keyword ?KW? List keywords, or say whether KW is one.", +]; +DISPATCHABLE_COMMAND( keyword ? 1 2 ){ + FILE *out = ISS(p)->out; + if( nArg<2 ){ + int i = 0; + int nk = sqlite3_keyword_count(); + int nCol = 0; + int szKW; + while( i75){ + zSep = "\n"; + nCol = 0; + } + memcpy(kwBuf, zKW, szKW); + kwBuf[szKW] = 0; + utf8_printf(out, "%s%s", kwBuf, zSep); + } + } + } + if( nCol>0 ) utf8_printf(out, "\n"); + }else{ + int szKW = strlen30(azArg[1]); + int isKeyword = sqlite3_keyword_check(azArg[1], szKW); + utf8_printf(out, "%s is%s a keyword\n", + azArg[1], (isKeyword)? "" : " not"); + } + return DCR_Ok; +} + +/***************** + * The .imposter, .iotrace, limit, lint and .log commands + */ +CONDITION_COMMAND( imposter !defined(SQLITE_OMIT_TEST_CONTROL) ); +CONDITION_COMMAND( iotrace defined(SQLITE_ENABLE_IOTRACE) ); +CONDITION_COMMAND( load !defined(SQLITE_OMIT_LOAD_EXTENSION) ); +COLLECT_HELP_TEXT[ + ".imposter INDEX TABLE Create imposter table TABLE on index INDEX", + ".iotrace FILE Enable I/O diagnostic logging to FILE", + ".limit ?LIMIT? ?VAL? Display or change the value of an SQLITE_LIMIT", + ".lint OPTIONS Report potential schema issues.", + " Options:", + " fkey-indexes Find missing foreign key indexes", + ".log FILE|off Turn logging on or off. FILE can be stderr/stdout", +]; +DISPATCHABLE_COMMAND( imposter ? 3 3 ){ + int rc = 0; + char *zSql; + char *zCollist = 0; + sqlite3_stmt *pStmt; + sqlite3 *db; + int tnum = 0; + int isWO = 0; /* True if making an imposter of a WITHOUT ROWID table */ + int lenPK = 0; /* Length of the PRIMARY KEY string for isWO tables */ + int i; + if( !(nArg==3 || (nArg==2 && sqlite3_stricmp(azArg[1],"off")==0)) ){ + *pzErr = smprintf("Usage: .imposter INDEX IMPOSTER\n" + " .imposter off\n"); + /* Also allowed, but not documented: + ** + ** .imposter TABLE IMPOSTER + ** + ** where TABLE is a WITHOUT ROWID table. In that case, the + ** imposter is another WITHOUT ROWID table with the columns in + ** storage order. */ + return DCR_SayUsage; + } + open_db(p, 0); + db = DBX(p); + if( nArg==2 ){ + sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, db, "main", 0, 1); + return DCR_Ok; + } + zSql = smprintf("SELECT rootpage, 0 FROM sqlite_schema" + " WHERE name='%q' AND type='index'" + "UNION ALL " + "SELECT rootpage, 1 FROM sqlite_schema" + " WHERE name='%q' AND type='table'" + " AND sql LIKE '%%without%%rowid%%'", + azArg[1], azArg[1]); + sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); + sqlite3_free(zSql); + if( sqlite3_step(pStmt)==SQLITE_ROW ){ + tnum = sqlite3_column_int(pStmt, 0); + isWO = sqlite3_column_int(pStmt, 1); + } + sqlite3_finalize(pStmt); + zSql = smprintf("PRAGMA index_xinfo='%q'", azArg[1]); + rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); + sqlite3_free(zSql); + i = 0; + while( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ + char zLabel[20]; + const char *zCol = (const char*)sqlite3_column_text(pStmt,2); + i++; + if( zCol==0 ){ + if( sqlite3_column_int(pStmt,1)==-1 ){ + zCol = "_ROWID_"; + }else{ + sqlite3_snprintf(sizeof(zLabel),zLabel,"expr%d",i); + zCol = zLabel; + } + } + if( isWO && lenPK==0 && sqlite3_column_int(pStmt,5)==0 && zCollist ){ + lenPK = (int)strlen(zCollist); + } + if( zCollist==0 ){ + zCollist = smprintf("\"%w\"", zCol); + }else{ + zCollist = smprintf("%z,\"%w\"", zCollist, zCol); + } + } + sqlite3_finalize(pStmt); + if( i==0 || tnum==0 ){ + *pzErr = smprintf("no such index: \"%s\"\n", azArg[1]); + sqlite3_free(zCollist); + return DCR_Error; + } + if( lenPK==0 ) lenPK = 100000; + zSql = smprintf("CREATE TABLE \"%w\"(%s,PRIMARY KEY(%.*s))" + "WITHOUT ROWID", azArg[2], zCollist, lenPK, zCollist); + sqlite3_free(zCollist); + rc = sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, db, "main", 1, tnum); if( rc==SQLITE_OK ){ - sqlite3_stmt *pStmt = 0; - /* ".recover" might output content in an order which causes immediate - ** foreign key constraints to be violated. So disable foreign-key - ** constraint enforcement to prevent problems when running the output - ** script. */ - raw_printf(pState->out, "PRAGMA foreign_keys=OFF;\n"); - raw_printf(pState->out, "BEGIN;\n"); - raw_printf(pState->out, "PRAGMA writable_schema = on;\n"); - shellPrepare(pState->db, &rc, - "SELECT sql FROM recovery.schema " - "WHERE type='table' AND sql LIKE 'create table%'", &pStmt - ); - while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ - const char *zCreateTable = (const char*)sqlite3_column_text(pStmt, 0); - raw_printf(pState->out, "CREATE TABLE IF NOT EXISTS %s;\n", - &zCreateTable[12] - ); + rc = sqlite3_exec(db, zSql, 0, 0, 0); + sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, db, "main", 0, 0); + if( rc ){ + *pzErr = smprintf("Error in [%s]: %s\n", zSql, sqlite3_errmsg(db)); + }else{ + utf8_printf(STD_OUT, "%s;\n", zSql); + raw_printf(STD_OUT, "WARNING: " + "writing to an imposter table will corrupt the \"%s\" %s!\n", + azArg[1], isWO ? "table" : "index" + ); } - shellFinalize(&rc, pStmt); + }else{ + *pzErr = smprintf("SQLITE_TESTCTRL_IMPOSTER returns %d\n", rc); } + sqlite3_free(zSql); + return rc != 0; +} +DISPATCHABLE_COMMAND( iotrace ? 2 2 ){ + SQLITE_API extern void (SQLITE_CDECL *sqlite3IoTrace)(const char*, ...); + if( iotrace && iotrace!=STD_OUT ) fclose(iotrace); + iotrace = 0; + if( nArg<2 ){ + sqlite3IoTrace = 0; + }else if( strcmp(azArg[1], "-")==0 ){ + sqlite3IoTrace = iotracePrintf; + iotrace = STD_OUT; + }else{ + iotrace = fopen(azArg[1], "w"); + if( iotrace==0 ){ + *pzErr = smprintf("cannot open \"%s\"\n", azArg[1]); + sqlite3IoTrace = 0; + return DCR_Error; + }else{ + sqlite3IoTrace = iotracePrintf; + } + } + return DCR_Ok; +} - /* Figure out if an orphan table will be required. And if so, how many - ** user columns it should contain */ - shellPrepare(pState->db, &rc, - "SELECT coalesce(max(maxlen), -2) FROM recovery.map WHERE root>1" - , &pLoop - ); - if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){ - nOrphan = sqlite3_column_int(pLoop, 0); +/***************** + * The .limits and .load commands + */ +COLLECT_HELP_TEXT[ + ",limits ?LIMIT_NAME? Display limit selected by its name, or all limits", + ".load FILE ?ENTRY? Load a SQLite extension library", + " If ENTRY is provided, the entry point \"sqlite_ENTRY_init\" is called.", + " Otherwise, the entry point name is derived from the FILE's name.", +]; + +DISPATCHABLE_COMMAND( limits 5 1 3 ){ + static const struct { + const char *zLimitName; /* Name of a limit */ + int limitCode; /* Integer code for that limit */ + } aLimit[] = { + { "length", SQLITE_LIMIT_LENGTH }, + { "sql_length", SQLITE_LIMIT_SQL_LENGTH }, + { "column", SQLITE_LIMIT_COLUMN }, + { "expr_depth", SQLITE_LIMIT_EXPR_DEPTH }, + { "compound_select", SQLITE_LIMIT_COMPOUND_SELECT }, + { "vdbe_op", SQLITE_LIMIT_VDBE_OP }, + { "function_arg", SQLITE_LIMIT_FUNCTION_ARG }, + { "attached", SQLITE_LIMIT_ATTACHED }, + { "like_pattern_length", SQLITE_LIMIT_LIKE_PATTERN_LENGTH }, + { "variable_number", SQLITE_LIMIT_VARIABLE_NUMBER }, + { "trigger_depth", SQLITE_LIMIT_TRIGGER_DEPTH }, + { "worker_threads", SQLITE_LIMIT_WORKER_THREADS }, + }; + int i, n2; + open_db(p, 0); + if( nArg==1 ){ + for(i=0; i3 ){ + return DCR_TooMany; + }else{ + int iLimit = -1; + n2 = strlen30(azArg[1]); + for(i=0; idb, &rc, - "SELECT pgno FROM recovery.map WHERE root=?", &pPages - ); +DISPATCHABLE_COMMAND( lint 3 1 0 ){ + sqlite3 *db; /* Database handle to query "main" db of */ + FILE *out = ISS(p)->out; /* Stream to write non-error output to */ + int bVerbose = 0; /* If -verbose is present */ + int bGroupByParent = 0; /* If -groupbyparent is present */ + int i; /* To iterate through azArg[] */ + const char *zIndent = ""; /* How much to indent CREATE INDEX by */ + int rc; /* Return code */ + sqlite3_stmt *pSql = 0; /* Compiled version of SQL statement below */ - shellPrepare(pState->db, &rc, - "SELECT max(field), group_concat(shell_escape_crnl(quote" - "(case when (? AND field<0) then NULL else value end)" - "), ', ')" - ", min(field) " - "FROM sqlite_dbdata WHERE pgno = ? AND field != ?" - "GROUP BY cell", &pCells - ); + i = (nArg>=2 ? strlen30(azArg[1]) : 0); + if( i==0 || 0!=sqlite3_strnicmp(azArg[1], "fkey-indexes", i) ){ + *pzErr = smprintf + ("Usage %s sub-command ?switches...?\n" + "Where sub-commands are:\n" + " fkey-indexes\n", azArg[0]); + return DCR_SayUsage; + } + open_db(p, 0); + db = DBX(p); - /* Loop through each root page. */ - shellPrepare(pState->db, &rc, - "SELECT root, intkey, max(maxlen) FROM recovery.map" - " WHERE root>1 GROUP BY root, intkey ORDER BY root=(" - " SELECT rootpage FROM recovery.schema WHERE name='sqlite_sequence'" - ")", &pLoop - ); - while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){ - int iRoot = sqlite3_column_int(pLoop, 0); - int bIntkey = sqlite3_column_int(pLoop, 1); - int nCol = sqlite3_column_int(pLoop, 2); - int bNoop = 0; - RecoverTable *pTab; + /* + ** This SELECT statement returns one row for each foreign key constraint + ** in the schema of the main database. The column values are: + ** + ** 0. The text of an SQL statement similar to: + ** + ** "EXPLAIN QUERY PLAN SELECT 1 FROM child_table WHERE child_key=?" + ** + ** This SELECT is similar to the one that the foreign keys implementation + ** needs to run internally on child tables. If there is an index that can + ** be used to optimize this query, then it can also be used by the FK + ** implementation to optimize DELETE or UPDATE statements on the parent + ** table. + ** + ** 1. A GLOB pattern suitable for sqlite3_strglob(). If the plan output by + ** the EXPLAIN QUERY PLAN command matches this pattern, then the schema + ** contains an index that can be used to optimize the query. + ** + ** 2. Human readable text that describes the child table and columns. e.g. + ** + ** "child_table(child_key1, child_key2)" + ** + ** 3. Human readable text that describes the parent table and columns. e.g. + ** + ** "parent_table(parent_key1, parent_key2)" + ** + ** 4. A full CREATE INDEX statement for an index that could be used to + ** optimize DELETE or UPDATE statements on the parent table. e.g. + ** + ** "CREATE INDEX child_table_child_key ON child_table(child_key)" + ** + ** 5. The name of the parent table. + ** + ** These six values are used by the C logic below to generate the report. + */ + const char *zSql = + "SELECT " + " 'EXPLAIN QUERY PLAN SELECT 1 FROM ' || quote(s.name) || ' WHERE '" + " || group_concat(quote(s.name) || '.' || quote(f.[from]) || '=?' " + " || fkey_collate_clause(" + " f.[table], COALESCE(f.[to], p.[name]), s.name, f.[from]),' AND ')" + ", " + " 'SEARCH ' || s.name || ' USING COVERING INDEX*('" + " || group_concat('*=?', ' AND ') || ')'" + ", " + " s.name || '(' || group_concat(f.[from], ', ') || ')'" + ", " + " f.[table] || '(' || group_concat(COALESCE(f.[to], p.[name])) || ')'" + ", " + " 'CREATE INDEX ' || quote(s.name ||'_'|| group_concat(f.[from], '_'))" + " || ' ON ' || quote(s.name) || '('" + " || group_concat(quote(f.[from]) ||" + " fkey_collate_clause(" + " f.[table], COALESCE(f.[to], p.[name]), s.name, f.[from]), ', ')" + " || ');'" + ", " + " f.[table] " + "FROM sqlite_schema AS s, pragma_foreign_key_list(s.name) AS f " + "LEFT JOIN pragma_table_info AS p ON (pk-1=seq AND p.arg=f.[table]) " + "GROUP BY s.name, f.id " + "ORDER BY (CASE WHEN ? THEN f.[table] ELSE s.name END)" + ; + const char *zGlobIPK = "SEARCH * USING INTEGER PRIMARY KEY (rowid=?)"; - assert( bIntkey==0 || bIntkey==1 ); - pTab = recoverFindTable(pState, &rc, iRoot, bIntkey, nCol, &bNoop); - if( bNoop || rc ) continue; - if( pTab==0 ){ - if( pOrphan==0 ){ - pOrphan = recoverOrphanTable(pState, &rc, zLostAndFound, nOrphan); - } - pTab = pOrphan; - if( pTab==0 ) break; + for(i=2; i1 && sqlite3_strnicmp("-verbose", azArg[i], n)==0 ){ + bVerbose = 1; } - - if( 0==sqlite3_stricmp(pTab->zQuoted, "\"sqlite_sequence\"") ){ - raw_printf(pState->out, "DELETE FROM sqlite_sequence;\n"); + else if( n>1 && sqlite3_strnicmp("-groupbyparent", azArg[i], n)==0 ){ + bGroupByParent = 1; + zIndent = " "; } - sqlite3_bind_int(pPages, 1, iRoot); - if( bRowids==0 && pTab->iPk<0 ){ - sqlite3_bind_int(pCells, 1, 1); - }else{ - sqlite3_bind_int(pCells, 1, 0); + else{ + raw_printf(STD_ERR, "Usage: %s %s ?-verbose? ?-groupbyparent?\n", + azArg[0], azArg[1] + ); + return DCR_Unknown|i; } - sqlite3_bind_int(pCells, 3, pTab->iPk); - - while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPages) ){ - int iPgno = sqlite3_column_int(pPages, 0); - sqlite3_bind_int(pCells, 2, iPgno); - while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pCells) ){ - int nField = sqlite3_column_int(pCells, 0); - int iMin = sqlite3_column_int(pCells, 2); - const char *zVal = (const char*)sqlite3_column_text(pCells, 1); + } - RecoverTable *pTab2 = pTab; - if( pTab!=pOrphan && (iMin<0)!=bIntkey ){ - if( pOrphan==0 ){ - pOrphan = recoverOrphanTable(pState, &rc, zLostAndFound, nOrphan); - } - pTab2 = pOrphan; - if( pTab2==0 ) break; - } + /* Register the fkey_collate_clause() SQL function */ + rc = sqlite3_create_function(db, "fkey_collate_clause", 4, SQLITE_UTF8, + 0, shellFkeyCollateClause, 0, 0 + ); - nField = nField+1; - if( pTab2==pOrphan ){ - raw_printf(pState->out, - "INSERT INTO %s VALUES(%d, %d, %d, %s%s%s);\n", - pTab2->zQuoted, iRoot, iPgno, nField, - iMin<0 ? "" : "NULL, ", zVal, pTab2->azlCol[nField] - ); - }else{ - raw_printf(pState->out, "INSERT INTO %s(%s) VALUES( %s );\n", - pTab2->zQuoted, pTab2->azlCol[nField], zVal - ); - } - } - shellReset(&rc, pCells); - } - shellReset(&rc, pPages); - if( pTab!=pOrphan ) recoverFreeTable(pTab); + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, zSql, -1, &pSql, 0); + } + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pSql, 1, bGroupByParent); } - shellFinalize(&rc, pLoop); - shellFinalize(&rc, pPages); - shellFinalize(&rc, pCells); - recoverFreeTable(pOrphan); - /* The rest of the schema */ if( rc==SQLITE_OK ){ - sqlite3_stmt *pStmt = 0; - shellPrepare(pState->db, &rc, - "SELECT sql, name FROM recovery.schema " - "WHERE sql NOT LIKE 'create table%'", &pStmt - ); - while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ - const char *zSql = (const char*)sqlite3_column_text(pStmt, 0); - if( sqlite3_strnicmp(zSql, "create virt", 11)==0 ){ - const char *zName = (const char*)sqlite3_column_text(pStmt, 1); - char *zPrint = shellMPrintf(&rc, - "INSERT INTO sqlite_schema VALUES('table', %Q, %Q, 0, %Q)", - zName, zName, zSql - ); - raw_printf(pState->out, "%s;\n", zPrint); - sqlite3_free(zPrint); + int rc2; + char *zPrev = 0; + while( SQLITE_ROW==sqlite3_step(pSql) ){ + int res = -1; + sqlite3_stmt *pExplain = 0; + const char *zEQP = (const char*)sqlite3_column_text(pSql, 0); + const char *zGlob = (const char*)sqlite3_column_text(pSql, 1); + const char *zFrom = (const char*)sqlite3_column_text(pSql, 2); + const char *zTarget = (const char*)sqlite3_column_text(pSql, 3); + const char *zCI = (const char*)sqlite3_column_text(pSql, 4); + const char *zParent = (const char*)sqlite3_column_text(pSql, 5); + + if( zEQP==0 || zGlob==0 ) continue; + rc = sqlite3_prepare_v2(db, zEQP, -1, &pExplain, 0); + if( rc!=SQLITE_OK ) break; + if( SQLITE_ROW==sqlite3_step(pExplain) ){ + const char *zPlan = (const char*)sqlite3_column_text(pExplain, 3); + res = zPlan!=0 && ( 0==sqlite3_strglob(zGlob, zPlan) + || 0==sqlite3_strglob(zGlobIPK, zPlan)); + } + rc = sqlite3_finalize(pExplain); + if( rc!=SQLITE_OK ) break; + + if( res<0 ){ + raw_printf(STD_ERR, "Error: internal error"); + break; }else{ - raw_printf(pState->out, "%s;\n", zSql); + if( bGroupByParent + && (bVerbose || res==0) + && (zPrev==0 || sqlite3_stricmp(zParent, zPrev)) + ){ + raw_printf(out, "-- Parent table %s\n", zParent); + sqlite3_free(zPrev); + zPrev = smprintf("%s", zParent); + } + + if( res==0 ){ + raw_printf(out, "%s%s --> %s\n", zIndent, zCI, zTarget); + }else if( bVerbose ){ + raw_printf(out, "%s/* no extra indexes required for %s -> %s */\n", + zIndent, zFrom, zTarget); + } } } - shellFinalize(&rc, pStmt); - } + sqlite3_free(zPrev); + + if( rc!=SQLITE_OK ){ + *pzErr = smprintf("%s\n", sqlite3_errmsg(db)); + } - if( rc==SQLITE_OK ){ - raw_printf(pState->out, "PRAGMA writable_schema = off;\n"); - raw_printf(pState->out, "COMMIT;\n"); + rc2 = sqlite3_finalize(pSql); + if( rc==SQLITE_OK && rc2!=SQLITE_OK ){ + rc = rc2; + *pzErr = smprintf("%s\n", sqlite3_errmsg(db)); + } + }else{ + *pzErr = smprintf("%s\n", sqlite3_errmsg(db)); } - sqlite3_exec(pState->db, "DETACH recovery", 0, 0, 0); - return rc; + + return DCR_Ok|(rc!=0); } -#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */ +DISPATCHABLE_COMMAND( load ? 2 3 ){ + const char *zFile = 0, *zProc = 0; + int ai = 1, rc; + if( ISS(p)->bSafeMode ) return DCR_AbortError; + while( ai Maybe init db, add column zCol to it. - * zAutoColumn(0, &db, ?) => (db!=0) Form columns spec for CREATE TABLE, - * close db and set it to 0, and return the columns spec, to later - * be sqlite3_free()'ed by the caller. - * The return is 0 when either: - * (a) The db was not initialized and zCol==0 (There are no columns.) - * (b) zCol!=0 (Column was added, db initialized as needed.) - * The 3rd argument, pRenamed, references an out parameter. If the - * pointer is non-zero, its referent will be set to a summary of renames - * done if renaming was necessary, or set to 0 if none was done. The out - * string (if any) must be sqlite3_free()'ed by the caller. - */ -#ifdef SHELL_DEBUG -#define rc_err_oom_die(rc) \ - if( rc==SQLITE_NOMEM ) shell_check_oom(0); \ - else if(!(rc==SQLITE_OK||rc==SQLITE_DONE)) \ - fprintf(stderr,"E:%d\n",rc), assert(0) -#else -static void rc_err_oom_die(int rc){ - if( rc==SQLITE_NOMEM ) shell_check_oom(0); - assert(rc==SQLITE_OK||rc==SQLITE_DONE); +DISPATCHABLE_COMMAND( log ? 2 2 ){ + const char *zFile = azArg[1]; + if( ISS(p)->bSafeMode ) return DCR_AbortError; + output_file_close(ISS(p)->pLog); + ISS(p)->pLog = output_file_open(zFile, 0); + return DCR_Ok; } -#endif -#ifdef SHELL_COLFIX_DB /* If this is set, the DB can be in a file. */ -static char zCOL_DB[] = SHELL_STRINGIFY(SHELL_COLFIX_DB); -#else /* Otherwise, memory is faster/better for the transient DB. */ -static const char *zCOL_DB = ":memory:"; -#endif +static void effectMode(ShellInState *psi, u8 modeRequest, u8 modeNominal){ + /* Effect the specified mode change. */ + const char *zColSep = 0, *zRowSep = 0; + assert(modeNominal!=MODE_COUNT_OF); + switch( modeRequest ){ + case MODE_Line: + zRowSep = SEP_Row; + break; + case MODE_Column: + if( (psi->shellFlgs & SHFLG_HeaderSet)==0 ){ + psi->showHeader = 1; + } + zRowSep = SEP_Row; + break; + case MODE_List: + zColSep = SEP_Column; + zRowSep = SEP_Row; + break; + case MODE_Html: + break; + case MODE_Tcl: + zColSep = SEP_Space; + zRowSep = SEP_Row; + break; + case MODE_Csv: + zColSep = SEP_Comma; + zRowSep = SEP_CrLf; + break; + case MODE_Tab: + zColSep = SEP_Tab; + break; + case MODE_Insert: + break; + case MODE_Quote: + zColSep = SEP_Comma; + zRowSep = SEP_Row; + break; + case MODE_Ascii: + zColSep = SEP_Unit; + zRowSep = SEP_Record; + break; + case MODE_Markdown: + /* fall-thru */ + case MODE_Table: + /* fall-thru */ + case MODE_Box: + break; + case MODE_Count: + /* fall-thru */ + case MODE_Off: + /* fall-thru */ + case MODE_Json: + break; + case MODE_Explain: case MODE_Semi: case MODE_EQP: case MODE_Pretty: default: + /* Modes used internally, not settable by .mode command. */ + return; + } + if( zRowSep!=0 ){ + sqlite3_snprintf(sizeof(psi->rowSeparator), psi->rowSeparator, zRowSep); + } + if( zColSep!=0 ){ + sqlite3_snprintf(sizeof(psi->colSeparator), psi->colSeparator, zColSep); + } + psi->mode = modeNominal; +} -/* Define character (as C string) to separate generated column ordinal - * from protected part of incoming column names. This defaults to "_" - * so that incoming column identifiers that did not need not be quoted - * remain usable without being quoted. It must be one character. +/***************** + * The .mode command */ -#ifndef SHELL_AUTOCOLUMN_SEP -# define AUTOCOLUMN_SEP "_" -#else -# define AUTOCOLUMN_SEP SHELL_STRINGIFY(SHELL_AUTOCOLUMN_SEP) -#endif - -static char *zAutoColumn(const char *zColNew, sqlite3 **pDb, char **pzRenamed){ - /* Queries and D{D,M}L used here */ - static const char * const zTabMake = "\ -CREATE TABLE ColNames(\ - cpos INTEGER PRIMARY KEY,\ - name TEXT, nlen INT, chop INT, reps INT, suff TEXT);\ -CREATE VIEW RepeatedNames AS \ -SELECT DISTINCT t.name FROM ColNames t \ -WHERE t.name COLLATE NOCASE IN (\ - SELECT o.name FROM ColNames o WHERE o.cpos<>t.cpos\ -);\ -"; - static const char * const zTabFill = "\ -INSERT INTO ColNames(name,nlen,chop,reps,suff)\ - VALUES(iif(length(?1)>0,?1,'?'),max(length(?1),1),0,0,'')\ -"; - static const char * const zHasDupes = "\ -SELECT count(DISTINCT (substring(name,1,nlen-chop)||suff) COLLATE NOCASE)\ - 1, printf('%c%0*d', '"AUTOCOLUMN_SEP"', $1, cpos), '')" -#else /* ...RENAME_MINIMAL_ONE_PASS */ -"WITH Lzn(nlz) AS (" /* Find minimum extraneous leading 0's for uniqueness */ -" SELECT 0 AS nlz" -" UNION" -" SELECT nlz+1 AS nlz FROM Lzn" -" WHERE EXISTS(" -" SELECT 1" -" FROM ColNames t, ColNames o" -" WHERE" -" iif(t.name IN (SELECT * FROM RepeatedNames)," -" printf('%s"AUTOCOLUMN_SEP"%s'," -" t.name, substring(printf('%.*c%0.*d',nlz+1,'0',$1,t.cpos),2))," -" t.name" -" )" -" =" -" iif(o.name IN (SELECT * FROM RepeatedNames)," -" printf('%s"AUTOCOLUMN_SEP"%s'," -" o.name, substring(printf('%.*c%0.*d',nlz+1,'0',$1,o.cpos),2))," -" o.name" -" )" -" COLLATE NOCASE" -" AND o.cpos<>t.cpos" -" GROUP BY t.cpos" -" )" -") UPDATE Colnames AS t SET" -" chop = 0," /* No chopping, never touch incoming names. */ -" suff = iif(name IN (SELECT * FROM RepeatedNames)," -" printf('"AUTOCOLUMN_SEP"%s', substring(" -" printf('%.*c%0.*d',(SELECT max(nlz) FROM Lzn)+1,'0',1,t.cpos),2))," -" ''" -" )" -#endif - ; - static const char * const zCollectVar = "\ -SELECT\ - '('||x'0a'\ - || group_concat(\ - cname||' TEXT',\ - ','||iif((cpos-1)%4>0, ' ', x'0a'||' '))\ - ||')' AS ColsSpec \ -FROM (\ - SELECT cpos, printf('\"%w\"',printf('%!.*s%s', nlen-chop,name,suff)) AS cname \ - FROM ColNames ORDER BY cpos\ -)"; - static const char * const zRenamesDone = - "SELECT group_concat(" - " printf('\"%w\" to \"%w\"',name,printf('%!.*s%s', nlen-chop, name, suff))," - " ','||x'0a')" - "FROM ColNames WHERE suff<>'' OR chop!=0" - ; - int rc; - sqlite3_stmt *pStmt = 0; - assert(pDb!=0); - if( zColNew ){ - /* Add initial or additional column. Init db if necessary. */ - if( *pDb==0 ){ - if( SQLITE_OK!=sqlite3_open(zCOL_DB, pDb) ) return 0; -#ifdef SHELL_COLFIX_DB - if(*zCOL_DB!=':') - sqlite3_exec(*pDb,"drop table if exists ColNames;" - "drop view if exists RepeatedNames;",0,0,0); -#endif - rc = sqlite3_exec(*pDb, zTabMake, 0, 0, 0); - rc_err_oom_die(rc); +COLLECT_HELP_TEXT[ + ".mode MODE ?OPTIONS? Set output mode", + " MODE is one of:", + " ascii Columns/rows delimited by 0x1F and 0x1E", + " box Tables using unicode box-drawing characters", + " csv Comma-separated values", + " column Output in columns. (See .width)", + " html HTML code", + " insert SQL insert statements for TABLE", + " json Results in a JSON array", + " line One value per line", + " list Values delimited by \"|\"", + " markdown Markdown table format", + " qbox Shorthand for \"box --width 60 --quote\"", + " quote Escape answers as for SQL", + " table ASCII-art table", + " tabs Tab-separated values", + " tcl TCL list elements", + " OPTIONS: (for columnar modes or insert mode):", + " --wrap N Wrap output lines to no longer than N characters", + " --wordwrap B Wrap or not at word boundaries per B (on/off)", + " --ww Shorthand for \"--wordwrap 1\"", + " --quote Quote output text as SQL literals", + " --noquote Do not quote output text", + " TABLE The name of SQL table used for \"insert\" mode", +]; +DISPATCHABLE_COMMAND( mode ? 1 0 ){ + ShellInState *psi = ISS(p); + const char *zTabname = 0; + const char *zArg; + int i, aix; + u8 foundMode = MODE_COUNT_OF, setMode = MODE_COUNT_OF; + ColModeOpts cmOpts = ColModeOpts_default; + for(aix=1; aixout; + const char *zMode; + int nms; + i = psi->mode; + assert(i>=0 && imode) ){ + raw_printf + (out, "current output mode: %.*s --wrap %d --wordwrap %s --%squote\n", + nms, zMode, psi->cmOpts.iWrap, + psi->cmOpts.bWordWrap ? "on" : "off", + psi->cmOpts.bQuote ? "" : "no"); + }else{ + raw_printf(out, "current output mode: %.*s\n", nms, zMode); } - assert(*pDb!=0); - rc = sqlite3_prepare_v2(*pDb, zTabFill, -1, &pStmt, 0); - rc_err_oom_die(rc); - rc = sqlite3_bind_text(pStmt, 1, zColNew, -1, 0); - rc_err_oom_die(rc); - rc = sqlite3_step(pStmt); - rc_err_oom_die(rc); - sqlite3_finalize(pStmt); - return 0; - }else if( *pDb==0 ){ - return 0; }else{ - /* Formulate the columns spec, close the DB, zero *pDb. */ - char *zColsSpec = 0; - int hasDupes = db_int(*pDb, zHasDupes); - int nDigits = (hasDupes)? db_int(*pDb, zColDigits) : 0; - if( hasDupes ){ -#ifdef SHELL_COLUMN_RENAME_CLEAN - 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); - assert(rc==SQLITE_DONE); + effectMode(psi, foundMode, setMode); + if( MODE_IS_COLUMNAR(setMode) ) psi->cmOpts = cmOpts; + else if( setMode==MODE_Insert ){ + set_table_name(p, zTabname ? zTabname : "table"); } - assert(db_int(*pDb, 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)); + } + psi->cMode = psi->mode; + return DCR_Ok; + flag_unknown: + *pzErr = smprintf("Unknown .mode option: %s\nValid options:\n%s", + zArg, + " --noquote\n" + " --quote\n" + " --wordwrap on/off\n" + " --wrap N\n" + " --ww\n"); + return DCR_Unknown|aix; + mode_unknown: + *pzErr = smprintf("Mode should be one of:\n" + " ascii box column csv html insert json line\n" + " list markdown qbox quote table tabs tcl\n"); + return DCR_Unknown|aix; + mode_badarg: + *pzErr = smprintf("Invalid .mode argument: %s\n", zArg); + return DCR_ArgWrong|aix; +} + +/***************** + * The .open, .nonce and .nullvalue commands + */ +COLLECT_HELP_TEXT[ + ".open ?OPTIONS? ?FILE? Close existing database and reopen FILE", + " Options:", + " --append Use appendvfs to append database to the end of FILE", +#ifndef SQLITE_OMIT_DESERIALIZE + " --deserialize Load into memory using sqlite3_deserialize()", + " --hexdb Load the output of \"dbtotxt\" as an in-memory db", + " --maxsize N Maximum size for --hexdb or --deserialized database", +#endif + " --new Initialize FILE to an empty database", + " --nofollow Do not follow symbolic links", + " --readonly Open FILE readonly", + " --zip FILE is a ZIP archive", + ".nonce STRING Suspend safe mode for one command if nonce matches", + ".nullvalue STRING Use STRING in place of NULL values", +]; +DISPATCHABLE_COMMAND( open 3 1 0 ){ + ShellInState *psi = ISS(p); + const char *zFN = 0; /* Pointer to constant filename */ + char *zNewFilename = 0; /* Name of the database file to open */ + int iName = 1; /* Index in azArg[] of the filename */ + int newFlag = 0; /* True to delete file before opening */ + u8 openMode = SHELL_OPEN_UNSPEC; + int openFlags = 0; + sqlite3_int64 szMax = 0; + int rc = 0; + /* Check for command-line arguments */ + for(iName=1; iNamepAuxDb->zDbFilename = 0; + sqlite3_free(psi->pAuxDb->zFreeOnClose); + psi->pAuxDb->zFreeOnClose = 0; + psi->openMode = openMode; + psi->openFlags = 0; + psi->szMax = 0; + + /* If a filename is specified, try to open it first */ + if( zFN || psi->openMode==SHELL_OPEN_HEXDB ){ + if( newFlag && zFN && !psi->bSafeMode ) shellDeleteFile(zFN); + if( psi->bSafeMode + && psi->openMode!=SHELL_OPEN_HEXDB + && zFN + && strcmp(zFN,":memory:")!=0 + ){ + *pzErr = smprintf("cannot open database files in safe mode"); + return DCR_AbortError; } - 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; - } + if( zFN ){ + zNewFilename = smprintf("%s", zFN); + shell_check_oom(zNewFilename); + }else{ + zNewFilename = 0; + } + psi->pAuxDb->zDbFilename = zNewFilename; + psi->openFlags = openFlags; + psi->szMax = szMax; + open_db(p, OPEN_DB_KEEPALIVE); + if( DBX(p)==0 ){ + *pzErr = smprintf("cannot open '%s'\n", zNewFilename); + sqlite3_free(zNewFilename); + rc = 1; + }else{ + psi->pAuxDb->zFreeOnClose = zNewFilename; } - sqlite3_finalize(pStmt); - sqlite3_close(*pDb); - *pDb = 0; - return zColsSpec; } + if( DBX(p)==0 ){ + /* As a fall-back open a TEMP database */ + psi->pAuxDb->zDbFilename = 0; + open_db(p, 0); + } + return DCR_Ok|(rc!=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; - int n, c; - int rc = 0; - char *azArg[52]; - -#ifndef SQLITE_OMIT_VIRTUALTABLE - if( p->expert.pExpert ){ - expertFinish(p, 1, 0); +DISPATCHABLE_COMMAND( nonce ? 2 2 ){ + ShellInState *psi = ISS(p); + if( psi->zNonce==0 || strcmp(azArg[1],psi->zNonce)!=0 ){ + raw_printf(STD_ERR, "line %d: incorrect nonce: \"%s\"\n", + psi->pInSource->lineno, azArg[1]); + exit(1); } + /* Suspend safe mode for 1 dot-command after this. */ + psi->bSafeModeFuture = 2; + return DCR_Ok; +} + +DISPATCHABLE_COMMAND( nullvalue ? 2 2 ){ + sqlite3_snprintf(sizeof(ISS(p)->nullValue), ISS(p)->nullValue, "%.*s", + (int)ArraySize(ISS(p)->nullValue)-1, azArg[1]); + return DCR_Ok; +} + +/* Helper functions for .parameter and .vars commands + */ + +struct keyval_row { char * value; int uses; int hits; }; + +static int kv_find_callback(void *pData, int nc, char **pV, char **pC){ + assert(nc>=1); + assert(strcmp(pC[0],"value")==0); + struct keyval_row *pParam = (struct keyval_row *)pData; + assert(pParam->value==0); /* key values are supposedly unique. */ + if( pParam->value!=0 ) sqlite3_free( pParam->value ); + pParam->value = smprintf("%s", pV[0]); /* source owned by statement */ + if( nc>1 ) pParam->uses = (int)integerValue(pV[1]); + ++pParam->hits; + return 0; +} + +static void append_in_clause(sqlite3_str *pStr, + const char **azBeg, const char **azLim); +static void append_glob_terms(sqlite3_str *pStr, const char *zColName, + const char **azBeg, const char **azLim); +static char *find_home_dir(int clearFlag); + +/* Create a home-relative pathname from ~ prefixed path. + * Return it, or 0 for any error. + * Caller must sqlite3_free() it. + */ +static char *home_based_path( const char *zPath ){ + char *zHome = find_home_dir(0); + char *zErr = 0; + assert( zPath[0]=='~' ); + if( zHome==0 ){ + zErr = "Cannot find home directory."; + }else if( zPath[0]==0 || (zPath[1]!='/' +#if defined(_WIN32) || defined(WIN32) + && zPath[1]!='\\' #endif + ) ){ + zErr = "Malformed pathname"; + }else{ + return smprintf("%s%s", zHome, zPath+1); + } + utf8_printf(STD_ERR, "Error: %s\n", zErr); + return 0; +} - /* Parse the input line into tokens. - */ - while( zLine[h] && nArgdb, shellAuth, p); - }else if( p->bSafeModePersist ){ - sqlite3_set_authorizer(p->db, safeModeAuth, p); - }else{ - sqlite3_set_authorizer(p->db, 0, 0); - } - }else -#endif + sbCopy = sqlite3_str_new(db); + sqlite3_str_appendf + (sbCopy, "INSERT OR REPLACE INTO %s(key,value,uses)" + "SELECT key, value, uses FROM %s WHERE key ", zTo, zFrom); + append_in_clause(sbCopy, azNames, azNames+nNames); + zSql = sqlite3_str_finish(sbCopy); + shell_check_oom(zSql); + rc = sqlite3_exec(db, zSql, 0, 0, 0); + sqlite3_free(zSql); -#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB) - if( c=='a' && strncmp(azArg[0], "archive", n)==0 ){ - open_db(p, 0); - failIfSafeMode(p, "cannot run .archive in safe mode"); - rc = arDotCommand(p, 0, azArg, nArg); - }else -#endif + sqlite3_exec(db, "DETACH "SH_KV_STORE_SCHEMA";", 0, 0, 0); + return rc; +} - if( (c=='b' && n>=3 && strncmp(azArg[0], "backup", n)==0) - || (c=='s' && n>=3 && strncmp(azArg[0], "save", n)==0) - ){ - const char *zDestFile = 0; - const char *zDb = 0; - sqlite3 *pDest; - sqlite3_backup *pBackup; - int j; - int bAsync = 0; - const char *zVfs = 0; - failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]); - for(j=1; jdb, zDb); - if( pBackup==0 ){ - utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(pDest)); - close_db(pDest); - return 1; - } - while( (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK ){} - sqlite3_backup_finish(pBackup); - if( rc==SQLITE_DONE ){ - rc = 0; - }else{ - utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(pDest)); - rc = 1; - } - close_db(pDest); - }else +/* Default locations of kv store DBs for .parameters and .vars save/load. */ +static const char *zDefaultParamStore = "~/sqlite_params.sdb"; +static const char *zDefaultVarStore = "~/sqlite_vars.sdb"; - if( c=='b' && n>=3 && strncmp(azArg[0], "bail", n)==0 ){ - if( nArg==2 ){ - bail_on_error = booleanValue(azArg[1]); - }else{ - raw_printf(stderr, "Usage: .bail on|off\n"); - rc = 1; - } - }else +/* Possibly generate a derived path from input spec, with defaulting + * and conversion of leading (or only) tilde as home directory. + * The above-set default is used for zSpec NULL, "" or "~". + * When return is 0, there is an error; what needs doing cannnot be done. + * If the return is exactly the input, it must not be sqlite3_free()'ed. + * If the return differs from the input, it must be sqlite3_free()'ed. + */ + static const char *kv_store_path(const char *zSpec, ParamTableUse ptu){ + if( zSpec==0 || zSpec[0]==0 || strcmp(zSpec,"~")==0 ){ + const char *zDef; + switch( ptu ){ + case PTU_Binding: zDef = zDefaultParamStore; break; + case PTU_Script: zDef = zDefaultVarStore; break; + default: return 0; + } + return home_based_path(zDef); + }else if ( zSpec[0]=='~' ){ + return home_based_path(zSpec); + } + return zSpec; +} + +/* Load some or all kv pairs. Arguments are "load FILE ?NAMES?". */ +static int kv_pairs_load(sqlite3 *db, ParamTableUse ptu, + const char *azArg[], int nArg){ + const char *zStore = kv_store_path((nArg>1)? azArg[1] : 0, ptu); + if( zStore==0 ){ + utf8_printf(STD_ERR, "Cannot form parameter load path. Nothing loaded.\n"); + return DCR_Error; + }else{ + const char **pzFirst = (nArg>2)? azArg+2 : 0; + int nNames = (nArg>2)? nArg-2 : 0; + int rc = kv_xfr_table(db, zStore, 0, ptu, pzFirst, nNames); + if( nArg>1 && zStore!=azArg[1] ) sqlite3_free((void*)zStore); + return rc; + } +} - if( c=='b' && n>=3 && strncmp(azArg[0], "binary", n)==0 ){ - if( nArg==2 ){ - if( booleanValue(azArg[1]) ){ - setBinaryMode(p->out, 1); - }else{ - setTextMode(p->out, 1); - } - }else{ - raw_printf(stderr, "Usage: .binary on|off\n"); - rc = 1; - } - }else +/* Save some or all parameters. Arguments are "save FILE ?NAMES?". */ +static int kv_pairs_save(sqlite3 *db, ParamTableUse ptu, + const char *azArg[], int nArg){ + const char *zStore = kv_store_path((nArg>1)? azArg[1] : 0, ptu); + if( zStore==0 ){ + utf8_printf(STD_ERR, "Cannot form parameter save path. Nothing saved.\n"); + return DCR_Error; + }else{ + const char **pzFirst = (nArg>2)? azArg+2 : 0; + int nNames = (nArg>2)? nArg-2 : 0; + int rc = kv_xfr_table(db, zStore, 1, ptu, pzFirst, nNames); + if( nArg>1 && zStore!=azArg[1] ) sqlite3_free((void*)zStore); + return rc; + } +} - /* The undocumented ".breakpoint" command causes a call to the no-op - ** routine named test_breakpoint(). - */ - if( c=='b' && n>=3 && strncmp(azArg[0], "breakpoint", n)==0 ){ - test_breakpoint(); - }else +#ifndef SQLITE_NOHAVE_SYSTEM +/* + * Edit one named value in the parameters or shell variables table. + * If it does not yet exist, create it. If eval is true, the value + * is treated as a bare expression, otherwise it is a text value. + * The "uses" argument sets the 3rd column in the selected table, + * and serves to select which of the two tables is modified. + */ +static int edit_one_kvalue(sqlite3 *db, char *name, int eval, + ParamTableUse uses, const char * zEditor){ + struct keyval_row kvRow = {0,0,0}; + int rc; + char * zVal = 0; + const char *zTab = 0; + char * zSql = 0; - if( c=='c' && strcmp(azArg[0],"cd")==0 ){ - failIfSafeMode(p, "cannot run .cd in safe mode"); - if( nArg==2 ){ -#if defined(_WIN32) || defined(WIN32) - wchar_t *z = sqlite3_win32_utf8_to_unicode(azArg[1]); - rc = !SetCurrentDirectoryW(z); - sqlite3_free(z); -#else - rc = chdir(azArg[1]); -#endif - if( rc ){ - utf8_printf(stderr, "Cannot change to directory \"%s\"\n", azArg[1]); - rc = 1; - } + switch( uses ){ + case PTU_Script: zTab = SHVAR_TABLE_SNAME; break; + case PTU_Binding: zTab = PARAM_TABLE_SNAME; break; + default: assert(0); + } + zSql = smprintf("SELECT value, uses FROM %s " + "WHERE key=%Q AND uses=%d", zTab, name, uses); + shell_check_oom(zSql); + sqlite3_exec(db, zSql, kv_find_callback, &kvRow, 0); + sqlite3_free(zSql); + assert(kvRow.hits<2); + if( kvRow.hits==1 && kvRow.uses==uses){ + /* Editing an existing value of same kind. */ + sqlite3_free(kvRow.value); + if( eval!=0 ){ + zSql = smprintf("SELECT edit(value, %Q) FROM %s " + "WHERE key=%Q AND uses=%d", zEditor, zTab, name, uses); + shell_check_oom(zSql); + zVal = db_text(db, zSql, 1); + sqlite3_free(zSql); + zSql = smprintf("UPDATE %s SET value=(SELECT %s) " + "WHERE key=%Q AND uses=%d", zTab, zVal, name, uses); }else{ - raw_printf(stderr, "Usage: .cd DIRECTORY\n"); - rc = 1; + zSql = smprintf("UPDATE %s SET value=edit(value, %Q) WHERE" + " key=%Q AND uses=%d", zTab, zEditor, name, uses); } - }else - - if( c=='c' && n>=3 && strncmp(azArg[0], "changes", n)==0 ){ - if( nArg==2 ){ - setOrClearFlag(p, SHFLG_CountChanges, azArg[1]); + }else{ + /* Editing a new value of same kind. */ + assert(kvRow.value==0 || kvRow.uses!=uses); + if( eval!=0 ){ + zSql = smprintf("SELECT edit('-- %q%s', %Q)", name, "\n", zEditor); + zVal = db_text(db, zSql, 1); + sqlite3_free(zSql); + zSql = smprintf("INSERT INTO %s(key,value,uses)" + " VALUES (%Q,(SELECT %s LIMIT 1),%d)", + zTab, name, zVal, uses); }else{ - raw_printf(stderr, "Usage: .changes on|off\n"); - rc = 1; + zSql = smprintf("INSERT INTO %s(key,value,uses)" + " VALUES (%Q,edit('-- %q;%s', %Q),%d)", + zTab, name, name, "\n", zEditor, uses); } - }else + } + shell_check_oom(zSql); + rc = sqlite3_exec(db, zSql, 0, 0, 0); + sqlite3_free(zSql); + sqlite3_free(zVal); + return rc!=SQLITE_OK; +} +#endif - /* Cancel output redirection, if it is currently set (by .testcase) - ** Then read the content of the testcase-out.txt file and compare against - ** azArg[1]. If there are differences, report an error and exit. - */ - if( c=='c' && n>=3 && strncmp(azArg[0], "check", n)==0 ){ - char *zRes = 0; - output_reset(p); - if( nArg!=2 ){ - raw_printf(stderr, "Usage: .check GLOB-PATTERN\n"); - rc = 2; - }else if( (zRes = readFile("testcase-out.txt", 0))==0 ){ - raw_printf(stderr, "Error: cannot read 'testcase-out.txt'\n"); - rc = 2; - }else if( testcase_glob(azArg[1],zRes)==0 ){ - utf8_printf(stderr, - "testcase-%s FAILED\n Expected: [%s]\n Got: [%s]\n", - p->zTestcase, azArg[1], zRes); - rc = 1; - }else{ - utf8_printf(stdout, "testcase-%s ok\n", p->zTestcase); - p->nCheck++; - } - sqlite3_free(zRes); - }else +/* Space-join values in an argument list. *valLim is not included. */ +char *values_join( char **valBeg, char **valLim ){ + char *z = 0; + const char *zSep = 0; + while( valBeg < valLim ){ + z = smprintf("%z%s%s", z, zSep, *valBeg); + zSep = " "; + ++valBeg; + } + return z; +} - if( c=='c' && strncmp(azArg[0], "clone", n)==0 ){ - failIfSafeMode(p, "cannot run .clone in safe mode"); - if( nArg==2 ){ - tryToClone(p, azArg[1]); - }else{ - raw_printf(stderr, "Usage: .clone FILENAME\n"); - rc = 1; - } - }else +static struct ParamSetOpts { + const char cCast; + const char *zTypename; + int evalKind; +} param_set_opts[] = { + /* { 'q', 0, 2 }, */ + /* { 'x', 0, 1 }, */ + { 'i', "INT", 1 }, + { 'r', "REAL", 1 }, + { 'b', "BLOB", 1 }, + { 't', "TEXT", 0 }, + { 'n', "NUMERIC", 1 } +}; - if( c=='c' && strncmp(azArg[0], "connection", n)==0 ){ - if( nArg==1 ){ - /* List available connections */ - int i; - for(i=0; iaAuxDb); i++){ - const char *zFile = p->aAuxDb[i].zDbFilename; - if( p->aAuxDb[i].db==0 && p->pAuxDb!=&p->aAuxDb[i] ){ - zFile = "(not open)"; - }else if( zFile==0 ){ - zFile = "(memory)"; - }else if( zFile[0]==0 ){ - zFile = "(temporary-file)"; - } - if( p->pAuxDb == &p->aAuxDb[i] ){ - utf8_printf(stdout, "ACTIVE %d: %s\n", i, zFile); - }else if( p->aAuxDb[i].db!=0 ){ - utf8_printf(stdout, " %d: %s\n", i, zFile); - } - } - }else if( nArg==2 && IsDigit(azArg[1][0]) && azArg[1][1]==0 ){ - int i = azArg[1][0] - '0'; - if( p->pAuxDb != &p->aAuxDb[i] && i>=0 && iaAuxDb) ){ - p->pAuxDb->db = p->db; - p->pAuxDb = &p->aAuxDb[i]; - globalDb = p->db = p->pAuxDb->db; - p->pAuxDb->db = 0; - } - }else if( nArg==3 && strcmp(azArg[1], "close")==0 - && IsDigit(azArg[2][0]) && azArg[2][1]==0 ){ - int i = azArg[2][0] - '0'; - if( i<0 || i>=ArraySize(p->aAuxDb) ){ - /* No-op */ - }else if( p->pAuxDb == &p->aAuxDb[i] ){ - raw_printf(stderr, "cannot close the active database connection\n"); - rc = 1; - }else if( p->aAuxDb[i].db ){ - session_close_all(p, i); - close_db(p->aAuxDb[i].db); - p->aAuxDb[i].db = 0; +/* Return an option character if it is single and prefixed by - or --, + * else return 0. + */ +static char option_char(char *zArg){ + if( zArg[0]=='-' ){ + ++zArg; + if( zArg[0]=='-' ) ++zArg; + if( zArg[0]!=0 && zArg[1]==0 ) return zArg[0]; + } + return 0; +} + +/* Most of .vars set + * Return SQLITE_OK on success, else SQLITE_ERROR. + */ +static int shvar_set(sqlite3 *db, char *name, char **valBeg, char **valLim){ + int rc = SQLITE_OK; + char *zValGlom = (valLim-valBeg>1)? values_join(valBeg, valLim) : 0; + sqlite3_stmt *pStmtSet = 0; + char *zValue = (zValGlom==0)? *valBeg : zValGlom; + char *zSql + = smprintf("REPLACE INTO "SHVAR_TABLE_SNAME"(key,value,uses)" + "VALUES(%Q,%Q,"SPTU_Script");", name, zValue); + shell_check_oom(zSql); + rc = sqlite3_prepare_v2(db, zSql, -1, &pStmtSet, 0); + assert(rc==SQLITE_OK); + sqlite3_free(zSql); + rc = (SQLITE_DONE==sqlite3_step(pStmtSet))? SQLITE_OK : SQLITE_ERROR; + sqlite3_finalize(pStmtSet); + sqlite3_free(zValGlom); + return rc; +} + + +/* Most of the .parameter set subcommand (per help text) + */ +static int param_set(sqlite3 *db, char cCast, + char *name, char **valBeg, char **valLim){ + char *zSql = 0; + int rc = SQLITE_OK, retries = 0, needsEval = 1; + char *zValGlom = (valLim-valBeg>1)? values_join(valBeg, valLim) : 0; + sqlite3_stmt *pStmtSet = 0; + const char *zCastTo = 0; + char *zValue = (zValGlom==0)? *valBeg : zValGlom; + if( cCast ){ + struct ParamSetOpts *pSO = param_set_opts; + for(; pSO-param_set_opts < ArraySize(param_set_opts); ++pSO ){ + if( cCast==pSO->cCast ){ + zCastTo = pSO->zTypename; + needsEval = pSO->evalKind > 0; + break; } + } + } + if( needsEval ){ + if( zCastTo!=0 ){ + zSql = smprintf + ( "REPLACE INTO "PARAM_TABLE_SNAME"(key,value,uses)" + " VALUES(%Q,CAST((%s) AS %s),"SPTU_Binding");", + name, zValue, zCastTo ); }else{ - raw_printf(stderr, "Usage: .connection [close] [CONNECTION-NUMBER]\n"); - rc = 1; + zSql = smprintf + ( "REPLACE INTO "PARAM_TABLE_SNAME"(key,value,uses)" + "VALUES(%Q,(%s),"SPTU_Binding");", name, zValue ); } - }else + shell_check_oom(zSql); + rc = sqlite3_prepare_v2(db, zSql, -1, &pStmtSet, 0); + sqlite3_free(zSql); + } + if( !needsEval || rc!=SQLITE_OK ){ + /* Reach here when value either requested to be cast to text, or must be. */ + sqlite3_finalize(pStmtSet); + pStmtSet = 0; + zSql = smprintf + ( "REPLACE INTO "PARAM_TABLE_SNAME"(key,value,uses)" + "VALUES(%Q,%Q,"SPTU_Binding");", name, zValue ); + shell_check_oom(zSql); + rc = sqlite3_prepare_v2(db, zSql, -1, &pStmtSet, 0); + assert(rc==SQLITE_OK); + sqlite3_free(zSql); + } + sqlite3_step(pStmtSet); + sqlite3_finalize(pStmtSet); + sqlite3_free(zValGlom); + return rc; +} - if( c=='d' && n>1 && strncmp(azArg[0], "databases", n)==0 ){ - char **azName = 0; - int nName = 0; - sqlite3_stmt *pStmt; - int i; - open_db(p, 0); - rc = sqlite3_prepare_v2(p->db, "PRAGMA database_list", -1, &pStmt, 0); - if( rc ){ - utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db)); - rc = 1; +/* list or ls subcommand for .parameter and .vars dot-commands */ +static void list_pov_entries(ShellExState *psx, ParamTableUse ptu, u8 bShort, + char **pzArgs, int nArg){ + sqlite3_stmt *pStmt = 0; + sqlite3_str *sbList; + int len = 0, rc; + char *zFromWhere = 0; + char *zSql = 0; + sqlite3 *db; + const char *zTab; + + switch( ptu ){ + case PTU_Binding: + db = DBX(psx); + zTab = PARAM_TABLE_SNAME; + break; + case PTU_Script: + db = psx->dbShell; + zTab = SHVAR_TABLE_NAME; + break; + default: assert(0); return; + } + sbList = sqlite3_str_new(db); + sqlite3_str_appendf(sbList, "FROM "); + sqlite3_str_appendf(sbList, zTab); + sqlite3_str_appendf(sbList, " WHERE (uses=?1) AND "); + append_glob_terms(sbList, "key", + (const char **)pzArgs, (const char **)pzArgs+nArg); + zFromWhere = sqlite3_str_finish(sbList); + shell_check_oom(zFromWhere); + zSql = smprintf("SELECT max(length(key)) %s", zFromWhere); + shell_check_oom(zSql); + rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); + if( rc==SQLITE_OK ){ + sqlite3_bind_int(pStmt, 1, ptu); + if( sqlite3_step(pStmt)==SQLITE_ROW ){ + len = sqlite3_column_int(pStmt, 0); + if( len>40 ) len = 40; + if( len<4 ) len = 4; + } + } + sqlite3_finalize(pStmt); + pStmt = 0; + if( len ){ + FILE *out = ISS(psx)->out; + sqlite3_free(zSql); + if( !bShort ){ + int nBindings = 0, nScripts = 0; + zSql = smprintf("SELECT key, uses," + " iif(typeof(value)='text', quote(value), value) as v" + " %z ORDER BY uses, key", zFromWhere); + shell_check_oom(zSql); + rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); + sqlite3_bind_int(pStmt, 1, ptu); + while( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ + ParamTableUse ptux = sqlite3_column_int(pStmt,1); + switch( ptux ){ + case PTU_Binding: + if( nBindings++ == 0 ){ + utf8_printf(out, "Bindings:\n%-*s %s\n", + len, "name", "value"); + } + utf8_printf(out, "%-*s %s\n", len, sqlite3_column_text(pStmt,0), + sqlite3_column_text(pStmt,2)); + break; + case PTU_Script: + if( nScripts++ == 0 ){ + utf8_printf(out, "Scripts:\n%-*s %s\n", len, "name", "value"); + } + utf8_printf(out, "%-*s %s\n", len, sqlite3_column_text(pStmt,0), + sqlite3_column_text(pStmt,2)); + break; + default: break; /* Ignore */ + } + } }else{ - while( sqlite3_step(pStmt)==SQLITE_ROW ){ - const char *zSchema = (const char *)sqlite3_column_text(pStmt,1); - const char *zFile = (const char*)sqlite3_column_text(pStmt,2); - if( zSchema==0 || zFile==0 ) continue; - azName = sqlite3_realloc(azName, (nName+1)*2*sizeof(char*)); - shell_check_oom(azName); - azName[nName*2] = strdup(zSchema); - azName[nName*2+1] = strdup(zFile); - nName++; + int nc = 0, ncw = 78/(len+2); + zSql = smprintf("SELECT key %z ORDER BY key", zFromWhere); + shell_check_oom(zSql); + rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); + sqlite3_bind_int(pStmt, 1, ptu); + while( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ + utf8_printf(out, "%s %-*s", ((++nc%ncw==0)? "\n" : ""), + len, sqlite3_column_text(pStmt,0)); } + if( nc>0 ) utf8_printf(out, "\n"); } sqlite3_finalize(pStmt); - for(i=0; idb, azName[i*2]); - int bRdonly = sqlite3_db_readonly(p->db, azName[i*2]); - const char *z = azName[i*2+1]; - utf8_printf(p->out, "%s: %s %s%s\n", - azName[i*2], - z && z[0] ? z : "\"\"", - bRdonly ? "r/o" : "r/w", - eTxn==SQLITE_TXN_NONE ? "" : - eTxn==SQLITE_TXN_READ ? " read-txn" : " write-txn"); - free(azName[i*2]); - free(azName[i*2+1]); - } - sqlite3_free(azName); - }else + }else{ + sqlite3_free(zFromWhere); + } + sqlite3_free(zSql); +} - if( c=='d' && n>=3 && strncmp(azArg[0], "dbconfig", n)==0 ){ - static const struct DbConfigChoices { - const char *zName; - int op; - } aDbConfig[] = { - { "defensive", SQLITE_DBCONFIG_DEFENSIVE }, - { "dqs_ddl", SQLITE_DBCONFIG_DQS_DDL }, - { "dqs_dml", SQLITE_DBCONFIG_DQS_DML }, - { "enable_fkey", SQLITE_DBCONFIG_ENABLE_FKEY }, - { "enable_qpsg", SQLITE_DBCONFIG_ENABLE_QPSG }, - { "enable_trigger", SQLITE_DBCONFIG_ENABLE_TRIGGER }, - { "enable_view", SQLITE_DBCONFIG_ENABLE_VIEW }, - { "fts3_tokenizer", SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER }, - { "legacy_alter_table", SQLITE_DBCONFIG_LEGACY_ALTER_TABLE }, - { "legacy_file_format", SQLITE_DBCONFIG_LEGACY_FILE_FORMAT }, - { "load_extension", SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION }, - { "no_ckpt_on_close", SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE }, - { "reset_database", SQLITE_DBCONFIG_RESET_DATABASE }, - { "trigger_eqp", SQLITE_DBCONFIG_TRIGGER_EQP }, - { "trusted_schema", SQLITE_DBCONFIG_TRUSTED_SCHEMA }, - { "writable_schema", SQLITE_DBCONFIG_WRITABLE_SCHEMA }, - }; - int ii, v; - open_db(p, 0); - for(ii=0; ii1 && strcmp(azArg[1], aDbConfig[ii].zName)!=0 ) continue; - if( nArg>=3 ){ - sqlite3_db_config(p->db, aDbConfig[ii].op, booleanValue(azArg[2]), 0); - } - sqlite3_db_config(p->db, aDbConfig[ii].op, -1, &v); - utf8_printf(p->out, "%19s %s\n", aDbConfig[ii].zName, v ? "on" : "off"); - if( nArg>1 ) break; +/* Append an OR'ed series of GLOB terms comparing a given column + * name to a series of patterns. Result is an appended expression. + * For an empty pattern series, expression is true for non-NULL. + */ +static void append_glob_terms(sqlite3_str *pStr, const char *zColName, + const char **azBeg, const char **azLim){ + if( azBeg==azLim ) sqlite3_str_appendf(pStr, "%s NOT NULL", zColName); + else{ + char *zSep = "("; + while( azBeg1 && ii==ArraySize(aDbConfig) ){ - utf8_printf(stderr, "Error: unknown dbconfig \"%s\"\n", azArg[1]); - utf8_printf(stderr, "Enter \".dbconfig\" with no arguments for a list\n"); - } - }else + sqlite3_str_appendf(pStr, ")"); + } +} - if( c=='d' && n>=3 && strncmp(azArg[0], "dbinfo", n)==0 ){ - rc = shell_dbinfo_command(p, nArg, azArg); - }else +/***************** + * The .parameter command + */ +COLLECT_HELP_TEXT[ + ".parameter CMD ... Manage per-DB SQL parameter bindings table", + " clear ?NAMES? Erase all or only given named parameters", +#ifndef SQLITE_NOHAVE_SYSTEM + " edit ?OPT? NAME ... Use edit() to create or alter parameter NAME", + " OPT may be -t to use edited value as text or -e to evaluate it.", +#endif + " init Initialize TEMP table for bindings and scripts", + " list ?PATTERNS? List current DB parameters table binding values", + " Alternatively, to list just some or all names: ls ?PATTERNS?", + " load ?FILE? ?NAMES? Load some or all named parameters from FILE", + " If FILE missing, empty or '~', it defaults to ~/sqlite_params.sdb", + " save ?FILE? ?NAMES? Save some or all named parameters into FILE", + " If FILE missing, empty or '~', it defaults to ~/sqlite_params.sdb", + " set ?TOPT? NAME VALUE Give SQL parameter NAME a value of VALUE", + " NAME must begin with one of $,:,@,? for bindings, VALUE is the space-", + " joined argument list. TOPT may be one of {-b -i -n -r -t} to cast the", + " effective value to BLOB, INT, NUMERIC, REAL or TEXT respectively.", + " unset ?NAMES? Remove named parameter(s) from parameters table", +]; +DISPATCHABLE_COMMAND( parameter 2 2 0 ){ + int rc = 0; + open_db(p,0); + sqlite3 *db = DBX(p); -#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) - if( c=='r' && strncmp(azArg[0], "recover", n)==0 ){ - open_db(p, 0); - rc = recoverDatabaseCmd(p, nArg, azArg); + /* .parameter clear and .parameter unset ?NAMES? + ** Delete some or all parameters from the TEMP table that holds them. + ** Without any arguments, clear deletes them all and unset does nothing. + */ + if( strcmp(azArg[1],"clear")==0 || strcmp(azArg[1],"unset")==0 ){ + if( param_table_exists(db) && (nArg>2 || azArg[1][0]=='c') ){ + sqlite3_str *sbZap = sqlite3_str_new(db); + char *zSql; + sqlite3_str_appendf + (sbZap, "DELETE FROM "PARAM_TABLE_SNAME" WHERE key "); + append_in_clause(sbZap, + (const char **)&azArg[2], (const char **)&azArg[nArg]); + zSql = sqlite3_str_finish(sbZap); + shell_check_oom(zSql); + sqlite3_exec(db, zSql, 0, 0, 0); + sqlite3_free(zSql); + } }else -#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */ - - if( c=='d' && strncmp(azArg[0], "dump", n)==0 ){ - char *zLike = 0; - char *zSql; - int i; - int savedShowHeader = p->showHeader; - int savedShellFlags = p->shellFlgs; - ShellClearFlag(p, - SHFLG_PreserveRowid|SHFLG_Newlines|SHFLG_Echo - |SHFLG_DumpDataOnly|SHFLG_DumpNoSys); - for(i=1; ipInSource) ){ + utf8_printf(STD_ERR, "Error: " + ".parameter edit can only be used interactively.\n"); + return DCR_Error; + } + param_table_init(db); + edSet = attempt_editor_set(psi, azArg[0], (nArg>2)? azArg[2] : 0 ); + if( edSet < 0 ) return DCR_Error; + else ia += edSet; + /* Future: Allow an option whereby new value can be evaluated + * the way that .parameter set ... does. + */ + while( ia < nArg ){ + ParamTableUse ptu; + char *zA = azArg[ia]; + char cf = (zA[0]=='-')? zA[1] : 0; + if( cf!=0 && zA[2]==0 ){ + ++ia; + switch( cf ){ + case 'e': eval = 1; continue; + case 't': eval = 0; continue; + default: + utf8_printf(STD_ERR, "Error: bad .parameter name: %s\n", zA); + return DCR_Error; } } + ptu = classify_param_name(zA); + if( ptu!=PTU_Binding ){ + utf8_printf(STD_ERR, "Error: " + "%s cannot be a binding parameter name.\n", zA); + return DCR_Error; + } + rc = edit_one_kvalue(db, zA, eval, ptu, psi->zEditor); + ++ia; + if( rc!=0 ) return DCR_Error; } - - open_db(p, 0); - - if( (p->shellFlgs & SHFLG_DumpDataOnly)==0 ){ - /* When playing back a "dump", the content might appear in an order - ** which causes immediate foreign key constraints to be violated. - ** So disable foreign-key constraint enforcement to prevent problems. */ - raw_printf(p->out, "PRAGMA foreign_keys=OFF;\n"); - raw_printf(p->out, "BEGIN TRANSACTION;\n"); - } - p->writableSchema = 0; - p->showHeader = 0; - /* Set writable_schema=ON since doing so forces SQLite to initialize - ** as much of the schema as it can even if the sqlite_schema table is - ** corrupt. */ - sqlite3_exec(p->db, "SAVEPOINT dump; PRAGMA writable_schema=ON", 0, 0, 0); - p->nErr = 0; - if( zLike==0 ) zLike = sqlite3_mprintf("true"); - zSql = sqlite3_mprintf( - "SELECT name, type, sql FROM sqlite_schema AS o " - "WHERE (%s) AND type=='table'" - " AND sql NOT NULL" - " ORDER BY tbl_name='sqlite_sequence', rowid", - zLike - ); - run_schema_dump_query(p,zSql); - sqlite3_free(zSql); - if( (p->shellFlgs & SHFLG_DumpDataOnly)==0 ){ - zSql = sqlite3_mprintf( - "SELECT sql FROM sqlite_schema AS o " - "WHERE (%s) AND sql NOT NULL" - " AND type IN ('index','trigger','view')", - zLike - ); - run_table_dump_query(p, zSql); - sqlite3_free(zSql); - } - sqlite3_free(zLike); - if( p->writableSchema ){ - raw_printf(p->out, "PRAGMA writable_schema=OFF;\n"); - p->writableSchema = 0; - } - sqlite3_exec(p->db, "PRAGMA writable_schema=OFF;", 0, 0, 0); - sqlite3_exec(p->db, "RELEASE dump;", 0, 0, 0); - if( (p->shellFlgs & SHFLG_DumpDataOnly)==0 ){ - raw_printf(p->out, p->nErr?"ROLLBACK; -- due to errors\n":"COMMIT;\n"); - } - p->showHeader = savedShowHeader; - p->shellFlgs = savedShellFlags; }else +#endif - if( c=='e' && strncmp(azArg[0], "echo", n)==0 ){ - if( nArg==2 ){ - setOrClearFlag(p, SHFLG_Echo, azArg[1]); - }else{ - raw_printf(stderr, "Usage: .echo on|off\n"); - rc = 1; - } + /* .parameter init + ** Make sure the TEMP table used to hold bind parameters exists. + ** Create it if necessary. + */ + if( nArg==2 && strcmp(azArg[1],"init")==0 ){ + param_table_init(db); }else - if( c=='e' && strncmp(azArg[0], "eqp", n)==0 ){ - if( nArg==2 ){ - p->autoEQPtest = 0; - if( p->autoEQPtrace ){ - if( p->db ) sqlite3_exec(p->db, "PRAGMA vdbe_trace=OFF;", 0, 0, 0); - p->autoEQPtrace = 0; - } - if( strcmp(azArg[1],"full")==0 ){ - p->autoEQP = AUTOEQP_full; - }else if( strcmp(azArg[1],"trigger")==0 ){ - p->autoEQP = AUTOEQP_trigger; -#ifdef SQLITE_DEBUG - }else if( strcmp(azArg[1],"test")==0 ){ - p->autoEQP = AUTOEQP_on; - p->autoEQPtest = 1; - }else if( strcmp(azArg[1],"trace")==0 ){ - p->autoEQP = AUTOEQP_full; - p->autoEQPtrace = 1; - open_db(p, 0); - sqlite3_exec(p->db, "SELECT name FROM sqlite_schema LIMIT 1", 0, 0, 0); - sqlite3_exec(p->db, "PRAGMA vdbe_trace=ON;", 0, 0, 0); -#endif - }else{ - p->autoEQP = (u8)booleanValue(azArg[1]); - } - }else{ - raw_printf(stderr, "Usage: .eqp off|on|trace|trigger|full\n"); - rc = 1; - } + /* .parameter list|ls + ** List all or selected bind parameters. + ** list displays names, values and uses. + ** ls displays just the names. + */ + if( nArg>=2 && ((strcmp(azArg[1],"list")==0) + || (strcmp(azArg[1],"ls")==0)) ){ + list_pov_entries(p, PTU_Binding, azArg[1][1]=='s', azArg+2, nArg-2); }else - if( c=='e' && strncmp(azArg[0], "exit", n)==0 ){ - if( nArg>1 && (rc = (int)integerValue(azArg[1]))!=0 ) exit(rc); - rc = 2; + /* .parameter load + ** Load all or named parameters from specified or default (DB) file. + */ + if( strcmp(azArg[1],"load")==0 ){ + param_table_init(db); + rc = kv_pairs_load(db, PTU_Binding, (const char **)azArg+1, nArg-1); }else - /* The ".explain" command is automatic now. It is largely pointless. It - ** retained purely for backwards compatibility */ - if( c=='e' && strncmp(azArg[0], "explain", n)==0 ){ - int val = 1; - if( nArg>=2 ){ - if( strcmp(azArg[1],"auto")==0 ){ - val = 99; - }else{ - val = booleanValue(azArg[1]); - } - } - if( val==1 && p->mode!=MODE_Explain ){ - p->normalMode = p->mode; - p->mode = MODE_Explain; - p->autoExplain = 0; - }else if( val==0 ){ - if( p->mode==MODE_Explain ) p->mode = p->normalMode; - p->autoExplain = 0; - }else if( val==99 ){ - if( p->mode==MODE_Explain ) p->mode = p->normalMode; - p->autoExplain = 1; - } + /* .parameter save + ** Save all or named parameters into specified or default (DB) file. + */ + if( strcmp(azArg[1],"save")==0 ){ + rc = kv_pairs_save(db, PTU_Binding, (const char **)azArg+1, nArg-1); }else -#ifndef SQLITE_OMIT_VIRTUALTABLE - if( c=='e' && strncmp(azArg[0], "expert", n)==0 ){ - if( p->bSafeMode ){ - raw_printf(stderr, - "Cannot run experimental commands such as \"%s\" in safe mode\n", - azArg[0]); + /* .parameter set NAME VALUE + ** Set or reset a bind parameter. NAME should be the full parameter + ** name exactly as it appears in the query. (ex: $abc, @def). The + ** VALUE can be in either SQL literal notation, or if not it will be + ** understood to be a text string. + */ + if( nArg>=4 && strcmp(azArg[1],"set")==0 ){ + char cCast = option_char(azArg[2]); + int inv = 2 + (cCast != 0); + ParamTableUse ptu = classify_param_name(azArg[inv]); + if( ptu!=PTU_Binding ){ + utf8_printf(STD_ERR, + "Error: %s is not a usable parameter name.\n", azArg[inv]); rc = 1; }else{ - open_db(p, 0); - expertDotCommand(p, azArg, nArg); + param_table_init(db); + rc = param_set(db, cCast, azArg[inv], &azArg[inv+1], &azArg[nArg]); + if( rc!=SQLITE_OK ){ + utf8_printf(STD_ERR, "Error: %s\n", sqlite3_errmsg(db)); + rc = 1; + } } }else -#endif - - if( c=='f' && strncmp(azArg[0], "filectrl", n)==0 ){ - static const struct { - const char *zCtrlName; /* Name of a test-control option */ - int ctrlCode; /* Integer code for that option */ - const char *zUsage; /* Usage notes */ - } aCtrl[] = { - { "chunk_size", SQLITE_FCNTL_CHUNK_SIZE, "SIZE" }, - { "data_version", SQLITE_FCNTL_DATA_VERSION, "" }, - { "has_moved", SQLITE_FCNTL_HAS_MOVED, "" }, - { "lock_timeout", SQLITE_FCNTL_LOCK_TIMEOUT, "MILLISEC" }, - { "persist_wal", SQLITE_FCNTL_PERSIST_WAL, "[BOOLEAN]" }, - /* { "pragma", SQLITE_FCNTL_PRAGMA, "NAME ARG" },*/ - { "psow", SQLITE_FCNTL_POWERSAFE_OVERWRITE, "[BOOLEAN]" }, - { "reserve_bytes", SQLITE_FCNTL_RESERVE_BYTES, "[N]" }, - { "size_limit", SQLITE_FCNTL_SIZE_LIMIT, "[LIMIT]" }, - { "tempfilename", SQLITE_FCNTL_TEMPFILENAME, "" }, - /* { "win32_av_retry", SQLITE_FCNTL_WIN32_AV_RETRY, "COUNT DELAY" },*/ - }; - int filectrl = -1; - int iCtrl = -1; - sqlite3_int64 iRes = 0; /* Integer result to display if rc2==1 */ - int isOk = 0; /* 0: usage 1: %lld 2: no-result */ - int n2, i; - const char *zCmd = 0; - const char *zSchema = 0; - - open_db(p, 0); - zCmd = nArg>=2 ? azArg[1] : "help"; - if( zCmd[0]=='-' - && (strcmp(zCmd,"--schema")==0 || strcmp(zCmd,"-schema")==0) - && nArg>=4 - ){ - zSchema = azArg[2]; - for(i=3; iout, "parameter", p); + return DCR_CmdErred; + } - /* The argument can optionally begin with "-" or "--" */ - if( zCmd[0]=='-' && zCmd[1] ){ - zCmd++; - if( zCmd[0]=='-' && zCmd[1] ) zCmd++; - } + return DCR_Ok | (rc!=0); +} - /* --help lists all file-controls */ - if( strcmp(zCmd,"help")==0 ){ - utf8_printf(p->out, "Available file-controls:\n"); - for(i=0; iout, " .filectrl %s %s\n", - aCtrl[i].zCtrlName, aCtrl[i].zUsage); +/***************** + * The .print, .progress and .prompt commands + */ +CONDITION_COMMAND( progress !defined(SQLITE_OMIT_PROGRESS_CALLBACK) ); +COLLECT_HELP_TEXT[ + ".print STRING... Print literal STRING", + ".progress N Invoke progress handler after every N opcodes", + " --limit N Interrupt after N progress callbacks", + " --once Do no more than one progress interrupt", + " --quiet|-q No output except at interrupts", + " --reset Reset the count for each input and interrupt", + ".prompt MAIN CONTINUE Replace the standard prompts", +]; +DISPATCHABLE_COMMAND( print 3 1 0 ){ + int i; + for(i=1; iout, "%s%s", azArg[i], (i==nArg-1)? "\n" : " "); + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( progress 3 2 0 ){ + ShellInState *psi = ISS(p); + int i; + int nn = 0; + psi->flgProgress = 0; + psi->mxProgress = 0; + psi->nProgress = 0; + for(i=1; iflgProgress |= SHELL_PROGRESS_QUIET; + continue; } - rc = 1; - goto meta_command_exit; - } - - /* convert filectrl text option to value. allow any unique prefix - ** of the option name, or a numerical value. */ - n2 = strlen30(zCmd); - for(i=0; iflgProgress |= SHELL_PROGRESS_RESET; + continue; + } + if( strcmp(z,"once")==0 ){ + psi->flgProgress |= SHELL_PROGRESS_ONCE; + continue; + } + if( strcmp(z,"limit")==0 ){ + if( i+1>=nArg ){ + *pzErr = smprintf("missing argument on --limit\n"); + return DCR_Unpaired|i; }else{ - utf8_printf(stderr, "Error: ambiguous file-control: \"%s\"\n" - "Use \".filectrl --help\" for help\n", zCmd); - rc = 1; - goto meta_command_exit; + psi->mxProgress = (int)integerValue(azArg[++i]); } + continue; } - } - if( filectrl<0 ){ - utf8_printf(stderr,"Error: unknown file-control: %s\n" - "Use \".filectrl --help\" for help\n", zCmd); + return DCR_Unknown|i; }else{ - switch(filectrl){ - case SQLITE_FCNTL_SIZE_LIMIT: { - if( nArg!=2 && nArg!=3 ) break; - iRes = nArg==3 ? integerValue(azArg[2]) : -1; - sqlite3_file_control(p->db, zSchema, SQLITE_FCNTL_SIZE_LIMIT, &iRes); - isOk = 1; - break; - } - case SQLITE_FCNTL_LOCK_TIMEOUT: - case SQLITE_FCNTL_CHUNK_SIZE: { - int x; - if( nArg!=3 ) break; - x = (int)integerValue(azArg[2]); - sqlite3_file_control(p->db, zSchema, filectrl, &x); - isOk = 2; - break; - } - case SQLITE_FCNTL_PERSIST_WAL: - case SQLITE_FCNTL_POWERSAFE_OVERWRITE: { - int x; - if( nArg!=2 && nArg!=3 ) break; - x = nArg==3 ? booleanValue(azArg[2]) : -1; - sqlite3_file_control(p->db, zSchema, filectrl, &x); - iRes = x; - isOk = 1; - break; - } - case SQLITE_FCNTL_DATA_VERSION: - case SQLITE_FCNTL_HAS_MOVED: { - int x; - if( nArg!=2 ) break; - sqlite3_file_control(p->db, zSchema, filectrl, &x); - iRes = x; - isOk = 1; - break; - } - case SQLITE_FCNTL_TEMPFILENAME: { - char *z = 0; - if( nArg!=2 ) break; - sqlite3_file_control(p->db, zSchema, filectrl, &z); - if( z ){ - utf8_printf(p->out, "%s\n", z); - sqlite3_free(z); - } - isOk = 2; - break; - } - case SQLITE_FCNTL_RESERVE_BYTES: { - int x; - if( nArg>=3 ){ - x = atoi(azArg[2]); - sqlite3_file_control(p->db, zSchema, filectrl, &x); - } - x = -1; - sqlite3_file_control(p->db, zSchema, filectrl, &x); - utf8_printf(p->out,"%d\n", x); - isOk = 2; - break; - } - } + nn = (int)integerValue(z); } - if( isOk==0 && iCtrl>=0 ){ - utf8_printf(p->out, "Usage: .filectrl %s %s\n", zCmd,aCtrl[iCtrl].zUsage); - rc = 1; - }else if( isOk==1 ){ - char zBuf[100]; - sqlite3_snprintf(sizeof(zBuf), zBuf, "%lld", iRes); - raw_printf(p->out, "%s\n", zBuf); + } + open_db(p, 0); + sqlite3_progress_handler(DBX(p), nn, progress_handler, psi); + return DCR_Ok; +} +/* Allow too few arguments by tradition, (a form of no-op.) */ +DISPATCHABLE_COMMAND( prompt ? 1 3 ){ + if( nArg >= 2) { + strncpy(mainPrompt,azArg[1],(int)ArraySize(mainPrompt)-1); + } + if( nArg >= 3) { + strncpy(continuePrompt,azArg[2],(int)ArraySize(continuePrompt)-1); + } + return DCR_Ok; +} + +/***************** + * The .recover and .restore commands + */ +CONDITION_COMMAND( recover !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) ); +COLLECT_HELP_TEXT[ + ".recover Recover as much data as possible from corrupt db.", + " --freelist-corrupt Assume the freelist is corrupt", + " --recovery-db NAME Store recovery metadata in database file NAME", + " --lost-and-found TABLE Alternative name for the lost-and-found table", + " --no-rowids Do not attempt to recover rowid values", + " that are not also INTEGER PRIMARY KEYs", + ".restore ?DB? FILE Restore content of DB (default \"main\") from FILE", +]; + +/* +** This command is invoked to recover data from the database. A script +** to construct a new database containing all recovered data is output +** on stream pState->out. +*/ +DISPATCHABLE_COMMAND( recover ? 1 7 ){ + FILE *out = ISS(p)->out; + sqlite3 *db; + int rc = SQLITE_OK; + sqlite3_stmt *pLoop = 0; /* Loop through all root pages */ + sqlite3_stmt *pPages = 0; /* Loop through all pages in a group */ + sqlite3_stmt *pCells = 0; /* Loop through all cells in a page */ + const char *zRecoveryDb = ""; /* Name of "recovery" database */ + const char *zLostAndFound = "lost_and_found"; + int i; + int nOrphan = -1; + RecoverTable *pOrphan = 0; + + open_db(p, 0); + db = DBX(p); + + int bFreelist = 1; /* 0 if --freelist-corrupt is specified */ + int bRowids = 1; /* 0 if --no-rowids */ + for(i=1; i0" + " UNION" + " SELECT shell_int32(" + " (SELECT data FROM sqlite_dbpage WHERE pgno=trunk.pgno), 0) AS x " + " FROM trunk WHERE x>0" + ")," + "freelist(data, n, freepgno) AS (" + " SELECT data, min(16384, shell_int32(data, 1)-1), t.pgno " + " FROM trunk t, sqlite_dbpage s WHERE s.pgno=t.pgno" + " UNION ALL" + " SELECT data, n-1, shell_int32(data, 2+n) " + " FROM freelist WHERE n>=0" + ")" + "REPLACE INTO recovery.freelist SELECT freepgno FROM freelist;" + ); + } + + /* If this is an auto-vacuum database, add all pointer-map pages to + ** the freelist table. Do this regardless of whether or not + ** --freelist-corrupt was specified. */ + shellExec(db, &rc, + "WITH ptrmap(pgno) AS (" + " SELECT 2 WHERE shell_int32(" + " (SELECT data FROM sqlite_dbpage WHERE pgno=1), 13" + " )" + " UNION ALL " + " SELECT pgno+1+(SELECT page_size FROM pragma_page_size)/5 AS pp " + " FROM ptrmap WHERE pp<=(SELECT page_count FROM pragma_page_count)" + ")" + "REPLACE INTO recovery.freelist SELECT pgno FROM ptrmap" + ); + + shellExec(db, &rc, + "CREATE TABLE recovery.dbptr(" + " pgno, child, PRIMARY KEY(child, pgno)" + ") WITHOUT ROWID;" + "INSERT OR IGNORE INTO recovery.dbptr(pgno, child) " + " SELECT * FROM sqlite_dbptr" + " WHERE pgno NOT IN freelist AND child NOT IN freelist;" - if( c=='f' && strncmp(azArg[0], "fullschema", n)==0 ){ - ShellState data; - int doStats = 0; - memcpy(&data, p, sizeof(data)); - data.showHeader = 0; - data.cMode = data.mode = MODE_Semi; - if( nArg==2 && optionMatch(azArg[1], "indent") ){ - data.cMode = data.mode = MODE_Pretty; - nArg = 1; - } - if( nArg!=1 ){ - raw_printf(stderr, "Usage: .fullschema ?--indent?\n"); - rc = 1; - goto meta_command_exit; - } - open_db(p, 0); - rc = sqlite3_exec(p->db, - "SELECT sql FROM" - " (SELECT sql sql, type type, tbl_name tbl_name, name name, rowid x" - " FROM sqlite_schema UNION ALL" - " SELECT sql, type, tbl_name, name, rowid FROM sqlite_temp_schema) " - "WHERE type!='meta' AND sql NOTNULL AND name NOT LIKE 'sqlite_%' " - "ORDER BY x", - callback, &data, 0 - ); - if( rc==SQLITE_OK ){ - sqlite3_stmt *pStmt; - rc = sqlite3_prepare_v2(p->db, - "SELECT rowid FROM sqlite_schema" - " WHERE name GLOB 'sqlite_stat[134]'", - -1, &pStmt, 0); - doStats = sqlite3_step(pStmt)==SQLITE_ROW; - sqlite3_finalize(pStmt); - } - if( doStats==0 ){ - raw_printf(p->out, "/* No STAT tables available */\n"); - }else{ - raw_printf(p->out, "ANALYZE sqlite_schema;\n"); - data.cMode = data.mode = MODE_Insert; - data.zDestTable = "sqlite_stat1"; - shell_exec(&data, "SELECT * FROM sqlite_stat1", 0); - data.zDestTable = "sqlite_stat4"; - shell_exec(&data, "SELECT * FROM sqlite_stat4", 0); - raw_printf(p->out, "ANALYZE sqlite_schema;\n"); - } - }else + /* Delete any pointer to page 1. This ensures that page 1 is considered + ** a root page, regardless of how corrupt the db is. */ + "DELETE FROM recovery.dbptr WHERE child = 1;" - if( c=='h' && strncmp(azArg[0], "headers", n)==0 ){ - if( nArg==2 ){ - p->showHeader = booleanValue(azArg[1]); - p->shellFlgs |= SHFLG_HeaderSet; - }else{ - raw_printf(stderr, "Usage: .headers on|off\n"); - rc = 1; - } - }else + /* Delete all pointers to any pages that have more than one pointer + ** to them. Such pages will be treated as root pages when recovering + ** data. */ + "DELETE FROM recovery.dbptr WHERE child IN (" + " SELECT child FROM recovery.dbptr GROUP BY child HAVING count(*)>1" + ");" - if( c=='h' && strncmp(azArg[0], "help", n)==0 ){ - if( nArg>=2 ){ - n = showHelp(p->out, azArg[1]); - if( n==0 ){ - utf8_printf(p->out, "Nothing matches '%s'\n", azArg[1]); - } - }else{ - showHelp(p->out, 0); - } - }else + /* Create the "map" table that will (eventually) contain instructions + ** for dealing with each page in the db that contains one or more + ** records. */ + "CREATE TABLE recovery.map(" + "pgno INTEGER PRIMARY KEY, maxlen INT, intkey, root INT" + ");" - if( c=='i' && strncmp(azArg[0], "import", n)==0 ){ - char *zTable = 0; /* Insert data into this table */ - char *zSchema = 0; /* within this schema (may default to "main") */ - char *zFile = 0; /* Name of file to extra content from */ - sqlite3_stmt *pStmt = NULL; /* A statement */ - int nCol; /* Number of columns in the table */ - int nByte; /* Number of bytes in an SQL string */ - int i, j; /* Loop counters */ - int needCommit; /* True to COMMIT or ROLLBACK at end */ - int nSep; /* Number of bytes in p->colSeparator[] */ - char *zSql; /* An SQL statement */ - char *zFullTabName; /* Table name with schema if applicable */ - ImportCtx sCtx; /* Reader context */ - char *(SQLITE_CDECL *xRead)(ImportCtx*); /* Func to read one value */ - int eVerbose = 0; /* Larger for more console output */ - int nSkip = 0; /* Initial lines to skip */ - int useOutputMode = 1; /* Use output mode to determine separators */ - char *zCreate = 0; /* CREATE TABLE statement text */ - - failIfSafeMode(p, "cannot run .import in safe mode"); - memset(&sCtx, 0, sizeof(sCtx)); - sCtx.z = sqlite3_malloc64(120); - if( sCtx.z==0 ){ - import_cleanup(&sCtx); - shell_out_of_memory(); - } - if( p->mode==MODE_Ascii ){ - xRead = ascii_read_one_field; - }else{ - xRead = csv_read_one_field; - } - for(i=1; iout, "ERROR: extra argument: \"%s\". Usage:\n", z); - showHelp(p->out, "import"); - rc = 1; - goto meta_command_exit; - } - }else if( strcmp(z,"-v")==0 ){ - eVerbose++; - }else if( strcmp(z,"-schema")==0 && iout, "ERROR: unknown option: \"%s\". Usage:\n", z); - showHelp(p->out, "import"); - rc = 1; - goto meta_command_exit; - } - } - if( zTable==0 ){ - utf8_printf(p->out, "ERROR: missing %s argument. Usage:\n", - zFile==0 ? "FILE" : "TABLE"); - showHelp(p->out, "import"); - rc = 1; - goto meta_command_exit; - } - seenInterrupt = 0; - open_db(p, 0); - if( useOutputMode ){ - /* If neither the --csv or --ascii options are specified, then set - ** the column and row separator characters from the output mode. */ - nSep = strlen30(p->colSeparator); - if( nSep==0 ){ - raw_printf(stderr, - "Error: non-null column separator required for import\n"); - rc = 1; - goto meta_command_exit; - } - if( nSep>1 ){ - raw_printf(stderr, - "Error: multi-character column separators not allowed" - " for import\n"); - rc = 1; - goto meta_command_exit; - } - nSep = strlen30(p->rowSeparator); - if( nSep==0 ){ - raw_printf(stderr, - "Error: non-null row separator required for import\n"); - rc = 1; - goto meta_command_exit; - } - if( nSep==2 && p->mode==MODE_Csv && strcmp(p->rowSeparator,SEP_CrLf)==0 ){ - /* When importing CSV (only), if the row separator is set to the - ** default output row separator, change it to the default input - ** row separator. This avoids having to maintain different input - ** and output row separators. */ - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row); - nSep = strlen30(p->rowSeparator); - } - if( nSep>1 ){ - raw_printf(stderr, "Error: multi-character row separators not allowed" - " for import\n"); - rc = 1; - goto meta_command_exit; - } - sCtx.cColSep = p->colSeparator[0]; - sCtx.cRowSep = p->rowSeparator[0]; - } - sCtx.zFile = zFile; - sCtx.nLine = 1; - if( sCtx.zFile[0]=='|' ){ -#ifdef SQLITE_OMIT_POPEN - raw_printf(stderr, "Error: pipes are not supported in this OS\n"); - rc = 1; - goto meta_command_exit; -#else - sCtx.in = popen(sCtx.zFile+1, "r"); - sCtx.zFile = ""; - sCtx.xCloser = pclose; -#endif - }else{ - sCtx.in = fopen(sCtx.zFile, "rb"); - sCtx.xCloser = fclose; - } - if( sCtx.in==0 ){ - utf8_printf(stderr, "Error: cannot open \"%s\"\n", zFile); - rc = 1; - import_cleanup(&sCtx); - goto meta_command_exit; - } - if( eVerbose>=2 || (eVerbose>=1 && useOutputMode) ){ - char zSep[2]; - zSep[1] = 0; - zSep[0] = sCtx.cColSep; - utf8_printf(p->out, "Column separator "); - output_c_string(p->out, zSep); - utf8_printf(p->out, ", row separator "); - zSep[0] = sCtx.cRowSep; - output_c_string(p->out, zSep); - utf8_printf(p->out, "\n"); - } - /* Below, resources must be freed before exit. */ - while( (nSkip--)>0 ){ - while( xRead(&sCtx) && sCtx.cTerm==sCtx.cColSep ){} - } - if( zSchema!=0 ){ - zFullTabName = sqlite3_mprintf("\"%w\".\"%w\"", zSchema, zTable); - }else{ - zFullTabName = sqlite3_mprintf("\"%w\"", zTable); - } - zSql = sqlite3_mprintf("SELECT * FROM %s", zFullTabName); - if( zSql==0 || zFullTabName==0 ){ - import_cleanup(&sCtx); - shell_out_of_memory(); - } - nByte = strlen30(zSql); - rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); - import_append_char(&sCtx, 0); /* To ensure sCtx.z is allocated */ - if( rc && sqlite3_strglob("no such table: *", sqlite3_errmsg(p->db))==0 ){ - sqlite3 *dbCols = 0; - char *zRenames = 0; - char *zColDefs; - zCreate = sqlite3_mprintf("CREATE TABLE %s", zFullTabName); - while( xRead(&sCtx) ){ - zAutoColumn(sCtx.z, &dbCols, 0); - if( sCtx.cTerm!=sCtx.cColSep ) break; - } - zColDefs = zAutoColumn(0, &dbCols, &zRenames); - if( zRenames!=0 ){ - utf8_printf((stdin_is_interactive && p->in==stdin)? p->out : stderr, - "Columns renamed during .import %s due to duplicates:\n" - "%s\n", sCtx.zFile, zRenames); - sqlite3_free(zRenames); - } - assert(dbCols==0); - if( zColDefs==0 ){ - utf8_printf(stderr,"%s: empty file\n", sCtx.zFile); - import_fail: - sqlite3_free(zCreate); - sqlite3_free(zSql); - sqlite3_free(zFullTabName); - import_cleanup(&sCtx); - rc = 1; - goto meta_command_exit; - } - zCreate = sqlite3_mprintf("%z%z\n", zCreate, zColDefs); - if( eVerbose>=1 ){ - utf8_printf(p->out, "%s\n", zCreate); - } - rc = sqlite3_exec(p->db, zCreate, 0, 0, 0); - if( rc ){ - utf8_printf(stderr, "%s failed:\n%s\n", zCreate, sqlite3_errmsg(p->db)); - goto import_fail; - } - sqlite3_free(zCreate); - zCreate = 0; - rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); - } - if( rc ){ - if (pStmt) sqlite3_finalize(pStmt); - utf8_printf(stderr,"Error: %s\n", sqlite3_errmsg(p->db)); - goto import_fail; - } - sqlite3_free(zSql); - nCol = sqlite3_column_count(pStmt); - sqlite3_finalize(pStmt); - pStmt = 0; - if( nCol==0 ) return 0; /* no columns, no error */ - zSql = sqlite3_malloc64( nByte*2 + 20 + nCol*2 ); - if( zSql==0 ){ - import_cleanup(&sCtx); - shell_out_of_memory(); - } - sqlite3_snprintf(nByte+20, zSql, "INSERT INTO %s VALUES(?", zFullTabName); - j = strlen30(zSql); - for(i=1; i=2 ){ - utf8_printf(p->out, "Insert using: %s\n", zSql); - } - rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); - if( rc ){ - utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db)); - if (pStmt) sqlite3_finalize(pStmt); - goto import_fail; - } - sqlite3_free(zSql); - sqlite3_free(zFullTabName); - needCommit = sqlite3_get_autocommit(p->db); - if( needCommit ) sqlite3_exec(p->db, "BEGIN", 0, 0, 0); - do{ - int startLine = sCtx.nLine; - for(i=0; imode==MODE_Ascii && (z==0 || z[0]==0) && i==0 ) break; - sqlite3_bind_text(pStmt, i+1, z, -1, SQLITE_TRANSIENT); - if( i=nCol ){ - sqlite3_step(pStmt); - rc = sqlite3_reset(pStmt); - if( rc!=SQLITE_OK ){ - utf8_printf(stderr, "%s:%d: INSERT failed: %s\n", sCtx.zFile, - startLine, sqlite3_errmsg(p->db)); - sCtx.nErr++; - }else{ - sCtx.nRow++; - } - } - }while( sCtx.cTerm!=EOF ); + /* Populate table [map]. If there are circular loops of pages in the + ** database, the following adds all pages in such a loop to the map + ** as individual root pages. This could be handled better. */ + "WITH pages(i, maxlen) AS (" + " SELECT page_count, (" + " SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=page_count" + " ) FROM pragma_page_count WHERE page_count>0" + " UNION ALL" + " SELECT i-1, (" + " SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=i-1" + " ) FROM pages WHERE i>=2" + ")" + "INSERT INTO recovery.map(pgno, maxlen, intkey, root) " + " SELECT i, maxlen, NULL, (" + " WITH p(orig, pgno, parent) AS (" + " SELECT 0, i, (SELECT pgno FROM recovery.dbptr WHERE child=i)" + " UNION " + " SELECT i, p.parent, " + " (SELECT pgno FROM recovery.dbptr WHERE child=p.parent) FROM p" + " )" + " SELECT pgno FROM p WHERE (parent IS NULL OR pgno = orig)" + ") " + "FROM pages WHERE maxlen IS NOT NULL AND i NOT IN freelist;" + "UPDATE recovery.map AS o SET intkey = (" + " SELECT substr(data, 1, 1)==X'0D' FROM sqlite_dbpage WHERE pgno=o.pgno" + ");" - import_cleanup(&sCtx); - sqlite3_finalize(pStmt); - if( needCommit ) sqlite3_exec(p->db, "COMMIT", 0, 0, 0); - if( eVerbose>0 ){ - utf8_printf(p->out, - "Added %d rows with %d errors using %d lines of input\n", - sCtx.nRow, sCtx.nErr, sCtx.nLine-1); - } - }else + /* Extract data from page 1 and any linked pages into table + ** recovery.schema. With the same schema as an sqlite_schema table. */ + "CREATE TABLE recovery.schema(type, name, tbl_name, rootpage, sql);" + "INSERT INTO recovery.schema SELECT " + " max(CASE WHEN field=0 THEN value ELSE NULL END)," + " max(CASE WHEN field=1 THEN value ELSE NULL END)," + " max(CASE WHEN field=2 THEN value ELSE NULL END)," + " max(CASE WHEN field=3 THEN value ELSE NULL END)," + " max(CASE WHEN field=4 THEN value ELSE NULL END)" + "FROM sqlite_dbdata WHERE pgno IN (" + " SELECT pgno FROM recovery.map WHERE root=1" + ")" + "GROUP BY pgno, cell;" + "CREATE INDEX recovery.schema_rootpage ON schema(rootpage);" + ); -#ifndef SQLITE_UNTESTABLE - if( c=='i' && strncmp(azArg[0], "imposter", n)==0 ){ - char *zSql; - char *zCollist = 0; - sqlite3_stmt *pStmt; - int tnum = 0; - int isWO = 0; /* True if making an imposter of a WITHOUT ROWID table */ - int lenPK = 0; /* Length of the PRIMARY KEY string for isWO tables */ - int i; - if( !(nArg==3 || (nArg==2 && sqlite3_stricmp(azArg[1],"off")==0)) ){ - utf8_printf(stderr, "Usage: .imposter INDEX IMPOSTER\n" - " .imposter off\n"); - /* Also allowed, but not documented: - ** - ** .imposter TABLE IMPOSTER - ** - ** where TABLE is a WITHOUT ROWID table. In that case, the - ** imposter is another WITHOUT ROWID table with the columns in - ** storage order. */ - rc = 1; - goto meta_command_exit; - } - open_db(p, 0); - if( nArg==2 ){ - sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 0, 1); - goto meta_command_exit; - } - zSql = sqlite3_mprintf( - "SELECT rootpage, 0 FROM sqlite_schema" - " WHERE name='%q' AND type='index'" - "UNION ALL " - "SELECT rootpage, 1 FROM sqlite_schema" - " WHERE name='%q' AND type='table'" - " AND sql LIKE '%%without%%rowid%%'", - azArg[1], azArg[1] + /* Open a transaction, then print out all non-virtual, non-"sqlite_%" + ** CREATE TABLE statements that extracted from the existing schema. */ + if( rc==SQLITE_OK ){ + sqlite3_stmt *pStmt = 0; + /* ".recover" might output content in an order which causes immediate + ** foreign key constraints to be violated. So disable foreign-key + ** constraint enforcement to prevent problems when running the output + ** script. */ + raw_printf(out, "PRAGMA foreign_keys=OFF;\n"); + raw_printf(out, "BEGIN;\n"); + raw_printf(out, "PRAGMA writable_schema = on;\n"); + shellPrepare(db, &rc, + "SELECT sql FROM recovery.schema " + "WHERE type='table' AND sql LIKE 'create table%'", &pStmt ); - sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - if( sqlite3_step(pStmt)==SQLITE_ROW ){ - tnum = sqlite3_column_int(pStmt, 0); - isWO = sqlite3_column_int(pStmt, 1); - } - sqlite3_finalize(pStmt); - zSql = sqlite3_mprintf("PRAGMA index_xinfo='%q'", azArg[1]); - rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - i = 0; - while( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ - char zLabel[20]; - const char *zCol = (const char*)sqlite3_column_text(pStmt,2); - i++; - if( zCol==0 ){ - if( sqlite3_column_int(pStmt,1)==-1 ){ - zCol = "_ROWID_"; - }else{ - sqlite3_snprintf(sizeof(zLabel),zLabel,"expr%d",i); - zCol = zLabel; - } - } - if( isWO && lenPK==0 && sqlite3_column_int(pStmt,5)==0 && zCollist ){ - lenPK = (int)strlen(zCollist); - } - if( zCollist==0 ){ - zCollist = sqlite3_mprintf("\"%w\"", zCol); - }else{ - zCollist = sqlite3_mprintf("%z,\"%w\"", zCollist, zCol); - } - } - sqlite3_finalize(pStmt); - if( i==0 || tnum==0 ){ - utf8_printf(stderr, "no such index: \"%s\"\n", azArg[1]); - rc = 1; - sqlite3_free(zCollist); - goto meta_command_exit; - } - if( lenPK==0 ) lenPK = 100000; - zSql = sqlite3_mprintf( - "CREATE TABLE \"%w\"(%s,PRIMARY KEY(%.*s))WITHOUT ROWID", - azArg[2], zCollist, lenPK, zCollist); - sqlite3_free(zCollist); - rc = sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 1, tnum); - if( rc==SQLITE_OK ){ - rc = sqlite3_exec(p->db, zSql, 0, 0, 0); - sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 0, 0); - if( rc ){ - utf8_printf(stderr, "Error in [%s]: %s\n", zSql, sqlite3_errmsg(p->db)); - }else{ - utf8_printf(stdout, "%s;\n", zSql); - raw_printf(stdout, - "WARNING: writing to an imposter table will corrupt the \"%s\" %s!\n", - azArg[1], isWO ? "table" : "index" - ); - } - }else{ - raw_printf(stderr, "SQLITE_TESTCTRL_IMPOSTER returns %d\n", rc); - rc = 1; + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *zCreateTable = (const char*)sqlite3_column_text(pStmt, 0); + raw_printf(out, "CREATE TABLE IF NOT EXISTS %s;\n", &zCreateTable[12]); } - sqlite3_free(zSql); - }else -#endif /* !defined(SQLITE_OMIT_TEST_CONTROL) */ + shellFinalize(&rc, pStmt); + } -#ifdef SQLITE_ENABLE_IOTRACE - if( c=='i' && strncmp(azArg[0], "iotrace", n)==0 ){ - SQLITE_API extern void (SQLITE_CDECL *sqlite3IoTrace)(const char*, ...); - if( iotrace && iotrace!=stdout ) fclose(iotrace); - iotrace = 0; - if( nArg<2 ){ - sqlite3IoTrace = 0; - }else if( strcmp(azArg[1], "-")==0 ){ - sqlite3IoTrace = iotracePrintf; - iotrace = stdout; - }else{ - iotrace = fopen(azArg[1], "w"); - if( iotrace==0 ){ - utf8_printf(stderr, "Error: cannot open \"%s\"\n", azArg[1]); - sqlite3IoTrace = 0; - rc = 1; - }else{ - sqlite3IoTrace = iotracePrintf; - } - } - }else -#endif + /* Figure out if an orphan table will be required. And if so, how many + ** user columns it should contain */ + shellPrepare(db, &rc, + "SELECT coalesce(max(maxlen), -2) FROM recovery.map WHERE root>1" + , &pLoop + ); + if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){ + nOrphan = sqlite3_column_int(pLoop, 0); + } + shellFinalize(&rc, pLoop); + pLoop = 0; - if( c=='l' && n>=5 && strncmp(azArg[0], "limits", n)==0 ){ - static const struct { - const char *zLimitName; /* Name of a limit */ - int limitCode; /* Integer code for that limit */ - } aLimit[] = { - { "length", SQLITE_LIMIT_LENGTH }, - { "sql_length", SQLITE_LIMIT_SQL_LENGTH }, - { "column", SQLITE_LIMIT_COLUMN }, - { "expr_depth", SQLITE_LIMIT_EXPR_DEPTH }, - { "compound_select", SQLITE_LIMIT_COMPOUND_SELECT }, - { "vdbe_op", SQLITE_LIMIT_VDBE_OP }, - { "function_arg", SQLITE_LIMIT_FUNCTION_ARG }, - { "attached", SQLITE_LIMIT_ATTACHED }, - { "like_pattern_length", SQLITE_LIMIT_LIKE_PATTERN_LENGTH }, - { "variable_number", SQLITE_LIMIT_VARIABLE_NUMBER }, - { "trigger_depth", SQLITE_LIMIT_TRIGGER_DEPTH }, - { "worker_threads", SQLITE_LIMIT_WORKER_THREADS }, - }; - int i, n2; - open_db(p, 0); - if( nArg==1 ){ - for(i=0; idb, aLimit[i].limitCode, -1)); - } - }else if( nArg>3 ){ - raw_printf(stderr, "Usage: .limit NAME ?NEW-VALUE?\n"); - rc = 1; - goto meta_command_exit; - }else{ - int iLimit = -1; - n2 = strlen30(azArg[1]); - for(i=0; idb, aLimit[iLimit].limitCode, - (int)integerValue(azArg[2])); + shellPrepare(db, &rc, + "SELECT pgno FROM recovery.map WHERE root=?", &pPages + ); + + shellPrepare(db, &rc, + "SELECT max(field), group_concat(shell_escape_crnl(quote" + "(case when (? AND field<0) then NULL else value end)" + "), ', ')" + ", min(field) " + "FROM sqlite_dbdata WHERE pgno = ? AND field != ?" + "GROUP BY cell", &pCells + ); + + /* Loop through each root page. */ + shellPrepare(db, &rc, + "SELECT root, intkey, max(maxlen) FROM recovery.map" + " WHERE root>1 GROUP BY root, intkey ORDER BY root=(" + " SELECT rootpage FROM recovery.schema WHERE name='sqlite_sequence'" + ")", &pLoop + ); + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){ + int iRoot = sqlite3_column_int(pLoop, 0); + int bIntkey = sqlite3_column_int(pLoop, 1); + int nCol = sqlite3_column_int(pLoop, 2); + int bNoop = 0; + RecoverTable *pTab; + + assert( bIntkey==0 || bIntkey==1 ); + pTab = recoverFindTable(db, &rc, iRoot, bIntkey, nCol, &bNoop); + if( bNoop || rc ) continue; + if( pTab==0 ){ + if( pOrphan==0 ){ + pOrphan = recoverOrphanTable(db, out, &rc, zLostAndFound, nOrphan); } - printf("%20s %d\n", aLimit[iLimit].zLimitName, - sqlite3_limit(p->db, aLimit[iLimit].limitCode, -1)); + pTab = pOrphan; + if( pTab==0 ) break; } - }else - - if( c=='l' && n>2 && strncmp(azArg[0], "lint", n)==0 ){ - open_db(p, 0); - lintDotCommand(p, azArg, nArg); - }else -#ifndef SQLITE_OMIT_LOAD_EXTENSION - if( c=='l' && strncmp(azArg[0], "load", n)==0 ){ - const char *zFile, *zProc; - char *zErrMsg = 0; - failIfSafeMode(p, "cannot run .load in safe mode"); - if( nArg<2 ){ - raw_printf(stderr, "Usage: .load FILE ?ENTRYPOINT?\n"); - rc = 1; - goto meta_command_exit; - } - zFile = azArg[1]; - zProc = nArg>=3 ? azArg[2] : 0; - open_db(p, 0); - rc = sqlite3_load_extension(p->db, zFile, zProc, &zErrMsg); - if( rc!=SQLITE_OK ){ - utf8_printf(stderr, "Error: %s\n", zErrMsg); - sqlite3_free(zErrMsg); - rc = 1; + if( 0==sqlite3_stricmp(pTab->zQuoted, "\"sqlite_sequence\"") ){ + raw_printf(out, "DELETE FROM sqlite_sequence;\n"); } - }else -#endif - - if( c=='l' && strncmp(azArg[0], "log", n)==0 ){ - failIfSafeMode(p, "cannot run .log in safe mode"); - if( nArg!=2 ){ - raw_printf(stderr, "Usage: .log FILENAME\n"); - rc = 1; + sqlite3_bind_int(pPages, 1, iRoot); + if( bRowids==0 && pTab->iPk<0 ){ + sqlite3_bind_int(pCells, 1, 1); }else{ - const char *zFile = azArg[1]; - output_file_close(p->pLog); - p->pLog = output_file_open(zFile, 0); + sqlite3_bind_int(pCells, 1, 0); } - }else + sqlite3_bind_int(pCells, 3, pTab->iPk); - if( c=='m' && strncmp(azArg[0], "mode", n)==0 ){ - const char *zMode = 0; - const char *zTabname = 0; - int i, n2; - ColModeOpts cmOpts = ColModeOpts_default; - for(i=1; izQuoted, iRoot, iPgno, nField, + iMin<0 ? "" : "NULL, ", zVal, pTab2->azlCol[nField] + ); + }else{ + raw_printf(out, "INSERT INTO %s(%s) VALUES( %s );\n", + pTab2->zQuoted, pTab2->azlCol[nField], zVal + ); } - }else if( zTabname==0 ){ - zTabname = z; - }else if( z[0]=='-' ){ - utf8_printf(stderr, "unknown option: %s\n", z); - utf8_printf(stderr, "options:\n" - " --noquote\n" - " --quote\n" - " --wordwrap on/off\n" - " --wrap N\n" - " --ww\n"); - rc = 1; - goto meta_command_exit; - }else{ - utf8_printf(stderr, "extra argument: \"%s\"\n", z); - rc = 1; - goto meta_command_exit; } + shellReset(&rc, pCells); } - if( zMode==0 ){ - if( p->mode==MODE_Column - || (p->mode>=MODE_Markdown && p->mode<=MODE_Box) - ){ - raw_printf - (p->out, - "current output mode: %s --wrap %d --wordwrap %s --%squote\n", - modeDescr[p->mode], p->cmOpts.iWrap, - p->cmOpts.bWordWrap ? "on" : "off", - p->cmOpts.bQuote ? "" : "no"); + shellReset(&rc, pPages); + if( pTab!=pOrphan ) recoverFreeTable(pTab); + } + shellFinalize(&rc, pLoop); + shellFinalize(&rc, pPages); + shellFinalize(&rc, pCells); + recoverFreeTable(pOrphan); + + /* The rest of the schema */ + if( rc==SQLITE_OK ){ + sqlite3_stmt *pStmt = 0; + shellPrepare(db, &rc, + "SELECT sql, name FROM recovery.schema " + "WHERE sql NOT LIKE 'create table%'", &pStmt + ); + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *zSql = (const char*)sqlite3_column_text(pStmt, 0); + if( sqlite3_strnicmp(zSql, "create virt", 11)==0 ){ + const char *zName = (const char*)sqlite3_column_text(pStmt, 1); + char *zPrint = shellMPrintf(&rc, + "INSERT INTO sqlite_schema VALUES('table', %Q, %Q, 0, %Q)", + zName, zName, zSql + ); + raw_printf(out, "%s;\n", zPrint); + sqlite3_free(zPrint); }else{ - raw_printf(p->out, "current output mode: %s\n", modeDescr[p->mode]); - } - zMode = modeDescr[p->mode]; - } - n2 = strlen30(zMode); - if( strncmp(zMode,"lines",n2)==0 ){ - p->mode = MODE_Line; - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row); - }else if( strncmp(zMode,"columns",n2)==0 ){ - p->mode = MODE_Column; - if( (p->shellFlgs & SHFLG_HeaderSet)==0 ){ - p->showHeader = 1; - } - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row); - p->cmOpts = cmOpts; - }else if( strncmp(zMode,"list",n2)==0 ){ - p->mode = MODE_List; - sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Column); - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row); - }else if( strncmp(zMode,"html",n2)==0 ){ - p->mode = MODE_Html; - }else if( strncmp(zMode,"tcl",n2)==0 ){ - p->mode = MODE_Tcl; - sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Space); - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row); - }else if( strncmp(zMode,"csv",n2)==0 ){ - p->mode = MODE_Csv; - sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma); - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_CrLf); - }else if( strncmp(zMode,"tabs",n2)==0 ){ - p->mode = MODE_List; - sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Tab); - }else if( strncmp(zMode,"insert",n2)==0 ){ - p->mode = MODE_Insert; - set_table_name(p, zTabname ? zTabname : "table"); - }else if( strncmp(zMode,"quote",n2)==0 ){ - p->mode = MODE_Quote; - sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma); - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row); - }else if( strncmp(zMode,"ascii",n2)==0 ){ - p->mode = MODE_Ascii; - sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Unit); - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Record); - }else if( strncmp(zMode,"markdown",n2)==0 ){ - p->mode = MODE_Markdown; - p->cmOpts = cmOpts; - }else if( strncmp(zMode,"table",n2)==0 ){ - p->mode = MODE_Table; - p->cmOpts = cmOpts; - }else if( strncmp(zMode,"box",n2)==0 ){ - p->mode = MODE_Box; - p->cmOpts = cmOpts; - }else if( strncmp(zMode,"count",n2)==0 ){ - p->mode = MODE_Count; - }else if( strncmp(zMode,"off",n2)==0 ){ - p->mode = MODE_Off; - }else if( strncmp(zMode,"json",n2)==0 ){ - p->mode = MODE_Json; - }else{ - raw_printf(stderr, "Error: mode should be one of: " - "ascii box column csv html insert json line list markdown " - "qbox quote table tabs tcl\n"); - rc = 1; + raw_printf(out, "%s;\n", zSql); + } } - p->cMode = p->mode; - }else + shellFinalize(&rc, pStmt); + } - if( c=='n' && strcmp(azArg[0], "nonce")==0 ){ - if( nArg!=2 ){ - raw_printf(stderr, "Usage: .nonce NONCE\n"); - rc = 1; - }else if( p->zNonce==0 || strcmp(azArg[1],p->zNonce)!=0 ){ - raw_printf(stderr, "line %d: incorrect nonce: \"%s\"\n", - p->lineno, azArg[1]); - exit(1); - }else{ - p->bSafeMode = 0; - return 0; /* Return immediately to bypass the safe mode reset - ** at the end of this procedure */ - } - }else + if( rc==SQLITE_OK ){ + raw_printf(out, "PRAGMA writable_schema = off;\n"); + raw_printf(out, "COMMIT;\n"); + } + sqlite3_exec(db, "DETACH recovery", 0, 0, 0); + return rc; +} - if( c=='n' && strncmp(azArg[0], "nullvalue", n)==0 ){ - if( nArg==2 ){ - sqlite3_snprintf(sizeof(p->nullValue), p->nullValue, - "%.*s", (int)ArraySize(p->nullValue)-1, azArg[1]); - }else{ - raw_printf(stderr, "Usage: .nullvalue STRING\n"); - rc = 1; +DISPATCHABLE_COMMAND( restore ? 2 3 ){ + int rc; + const char *zSrcFile; + const char *zDb; + sqlite3 *pSrc; + sqlite3_backup *pBackup; + int nTimeout = 0; + + if( ISS(p)->bSafeMode ) return DCR_AbortError; + if( nArg==2 ){ + zSrcFile = azArg[1]; + zDb = "main"; + }else if( nArg==3 ){ + zSrcFile = azArg[2]; + zDb = azArg[1]; + }else{ + return DCR_TooMany; + } + rc = sqlite3_open(zSrcFile, &pSrc); + if( rc!=SQLITE_OK ){ + *pzErr = smprintf("cannot open \"%s\"\n", zSrcFile); + close_db(pSrc); + return DCR_Error; + } + open_db(p, 0); + pBackup = sqlite3_backup_init(DBX(p), zDb, pSrc, "main"); + if( pBackup==0 ){ + *pzErr = smprintf("%s\n", sqlite3_errmsg(DBX(p))); + close_db(pSrc); + return DCR_Error; + } + while( (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK + || rc==SQLITE_BUSY ){ + if( rc==SQLITE_BUSY ){ + if( nTimeout++ >= 3 ) break; + sqlite3_sleep(100); } - }else + } + sqlite3_backup_finish(pBackup); + if( rc==SQLITE_DONE ){ + rc = 0; + }else if( rc==SQLITE_BUSY || rc==SQLITE_LOCKED ){ + *pzErr = smprintf("source database is busy\n"); + rc = 1; + }else{ + *pzErr = smprintf("%s\n", sqlite3_errmsg(DBX(p))); + rc = 1; + } + close_db(pSrc); + return DCR_Ok|rc; +} - if( c=='o' && strncmp(azArg[0], "open", n)==0 && n>=2 ){ - const char *zFN = 0; /* Pointer to constant filename */ - char *zNewFilename = 0; /* Name of the database file to open */ - int iName = 1; /* Index in azArg[] of the filename */ - int newFlag = 0; /* True to delete file before opening */ - int openMode = SHELL_OPEN_UNSPEC; - - /* Check for command-line arguments */ - for(iName=1; iNameopenFlags |= SQLITE_OPEN_NOFOLLOW; -#ifndef SQLITE_OMIT_DESERIALIZE - }else if( optionMatch(z, "deserialize") ){ - openMode = SHELL_OPEN_DESERIALIZE; - }else if( optionMatch(z, "hexdb") ){ - openMode = SHELL_OPEN_HEXDB; - }else if( optionMatch(z, "maxsize") && iName+1szMax = integerValue(azArg[++iName]); -#endif /* SQLITE_OMIT_DESERIALIZE */ - }else if( z[0]=='-' ){ - utf8_printf(stderr, "unknown option: %s\n", z); - rc = 1; - goto meta_command_exit; - }else if( zFN ){ - utf8_printf(stderr, "extra argument: \"%s\"\n", z); - rc = 1; - goto meta_command_exit; - }else{ - zFN = z; - } - } - - /* Close the existing database */ - session_close_all(p, -1); - close_db(p->db); - p->db = 0; - p->pAuxDb->zDbFilename = 0; - sqlite3_free(p->pAuxDb->zFreeOnClose); - p->pAuxDb->zFreeOnClose = 0; - p->openMode = openMode; - p->openFlags = 0; - p->szMax = 0; - - /* If a filename is specified, try to open it first */ - if( zFN || p->openMode==SHELL_OPEN_HEXDB ){ - if( newFlag && zFN && !p->bSafeMode ) shellDeleteFile(zFN); - if( p->bSafeMode - && p->openMode!=SHELL_OPEN_HEXDB - && zFN - && strcmp(zFN,":memory:")!=0 - ){ - failIfSafeMode(p, "cannot open disk-based database files in safe mode"); - } - if( zFN ){ - zNewFilename = sqlite3_mprintf("%s", zFN); - shell_check_oom(zNewFilename); - }else{ - zNewFilename = 0; - } - p->pAuxDb->zDbFilename = zNewFilename; - open_db(p, OPEN_DB_KEEPALIVE); - if( p->db==0 ){ - utf8_printf(stderr, "Error: cannot open '%s'\n", zNewFilename); - sqlite3_free(zNewFilename); - }else{ - p->pAuxDb->zFreeOnClose = zNewFilename; - } - } - if( p->db==0 ){ - /* As a fall-back open a TEMP database */ - p->pAuxDb->zDbFilename = 0; - open_db(p, 0); - } - }else +/***************** + * The .scanstats and .schema commands + */ +COLLECT_HELP_TEXT[ + ".scanstats on|off Turn sqlite3_stmt_scanstatus() metrics on or off", + ".schema ?PATTERN? Show the CREATE statements matching PATTERN", + " Options:", + " --indent Try to pretty-print the schema", + " --nosys Omit objects whose names start with \"sqlite_\"", +]; +DISPATCHABLE_COMMAND( scanstats ? 2 2 ){ + ISS(p)->scanstatsOn = (u8)booleanValue(azArg[1]); +#ifndef SQLITE_ENABLE_STMT_SCANSTATUS + raw_printf(STD_ERR, "Warning: .scanstats not available in this build.\n"); +#endif + return DCR_Ok; +} +DISPATCHABLE_COMMAND( schema ? 1 2 ){ - int rc; ++ int rc = 0; + ShellText sSelect; + ShellInState data; + ShellExState datax; + char *zErrMsg = 0; + const char *zDiv = "("; + const char *zName = 0; + int iSchema = 0; + int bDebug = 0; + int bNoSystemTabs = 0; + int ii; - if( (c=='o' - && (strncmp(azArg[0], "output", n)==0||strncmp(azArg[0], "once", n)==0)) - || (c=='e' && n==5 && strcmp(azArg[0],"excel")==0) - ){ - char *zFile = 0; - int bTxtMode = 0; - int i; - int eMode = 0; - int bOnce = 0; /* 0: .output, 1: .once, 2: .excel */ - unsigned char zBOM[4]; /* Byte-order mark to using if --bom is present */ - - zBOM[0] = 0; - failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]); - if( c=='e' ){ - eMode = 'x'; - bOnce = 2; - }else if( strncmp(azArg[0],"once",n)==0 ){ - bOnce = 1; - } - for(i=1; iout, "ERROR: unknown option: \"%s\". Usage:\n", - azArg[i]); - showHelp(p->out, azArg[0]); - rc = 1; - goto meta_command_exit; - } - }else if( zFile==0 && eMode!='e' && eMode!='x' ){ - zFile = sqlite3_mprintf("%s", z); - if( zFile && zFile[0]=='|' ){ - while( i+1out,"ERROR: extra parameter: \"%s\". Usage:\n", - azArg[i]); - showHelp(p->out, azArg[0]); - rc = 1; - sqlite3_free(zFile); - goto meta_command_exit; - } - } - if( zFile==0 ){ - zFile = sqlite3_mprintf("stdout"); - } - if( bOnce ){ - p->outCount = 2; + open_db(p, 0); + /* Consider some refactoring to avoid duplicative wholesale copying. */ + memcpy(&data, ISS(p), sizeof(data)); + memcpy(&datax, p, sizeof(datax)); + data.pSXS = &datax; + datax.pSIS = &data; + + data.showHeader = 0; + data.cMode = data.mode = MODE_Semi; + initText(&sSelect); + for(ii=1; iioutCount = 0; + return DCR_TooMany; + } + } + if( zName!=0 ){ + int isSchema = sqlite3_strlike(zName, "sqlite_master", '\\')==0 + || sqlite3_strlike(zName, "sqlite_schema", '\\')==0 + || sqlite3_strlike(zName,"sqlite_temp_master", '\\')==0 + || sqlite3_strlike(zName,"sqlite_temp_schema", '\\')==0; + if( isSchema ){ + char *new_argv[2], *new_colv[2]; + new_argv[0] = smprintf( + "CREATE TABLE %s (\n" + " type text,\n" + " name text,\n" + " tbl_name text,\n" + " rootpage integer,\n" + " sql text\n" + ")", zName); + shell_check_oom(new_argv[0]); + new_argv[1] = 0; + new_colv[0] = "sql"; + new_colv[1] = 0; + callback(&datax, 1, new_argv, new_colv); + sqlite3_free(new_argv[0]); + } + } + if( zDiv ){ + sqlite3_stmt *pStmt = 0; + rc = sqlite3_prepare_v2(datax.dbUser, + "SELECT name FROM pragma_database_list", + -1, &pStmt, 0); + if( rc ){ + *pzErr = smprintf("%s\n", sqlite3_errmsg(datax.dbUser)); + sqlite3_finalize(pStmt); + return DCR_Error; } - output_reset(p); -#ifndef SQLITE_NOHAVE_SYSTEM - if( eMode=='e' || eMode=='x' ){ - p->doXdgOpen = 1; - outputModePush(p); - if( eMode=='x' ){ - /* spreadsheet mode. Output as CSV. */ - newTempFile(p, "csv"); - ShellClearFlag(p, SHFLG_Echo); - p->mode = MODE_Csv; - sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma); - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_CrLf); + appendText(&sSelect, "SELECT sql FROM", 0); + iSchema = 0; + while( sqlite3_step(pStmt)==SQLITE_ROW ){ + const char *zDb = (const char*)sqlite3_column_text(pStmt, 0); + char zScNum[30]; + sqlite3_snprintf(sizeof(zScNum), zScNum, "%d", ++iSchema); + appendText(&sSelect, zDiv, 0); + zDiv = " UNION ALL "; + appendText(&sSelect, "SELECT shell_add_schema(sql,", 0); + if( sqlite3_stricmp(zDb, "main")!=0 ){ + appendText(&sSelect, zDb, '\''); }else{ - /* text editor mode */ - newTempFile(p, "txt"); - bTxtMode = 1; + appendText(&sSelect, "NULL", 0); } - sqlite3_free(zFile); - zFile = sqlite3_mprintf("%s", p->zTempFile); + appendText(&sSelect, ",name) AS sql, type, tbl_name, name, rowid,", 0); + appendText(&sSelect, zScNum, 0); + appendText(&sSelect, " AS snum, ", 0); + appendText(&sSelect, zDb, '\''); + appendText(&sSelect, " AS sname FROM ", 0); + appendText(&sSelect, zDb, quoteChar(zDb)); + appendText(&sSelect, ".sqlite_schema", 0); } -#endif /* SQLITE_NOHAVE_SYSTEM */ - shell_check_oom(zFile); - if( zFile[0]=='|' ){ -#ifdef SQLITE_OMIT_POPEN - raw_printf(stderr, "Error: pipes are not supported in this OS\n"); - rc = 1; - p->out = stdout; -#else - p->out = popen(zFile + 1, "w"); - if( p->out==0 ){ - utf8_printf(stderr,"Error: cannot open pipe \"%s\"\n", zFile + 1); - p->out = stdout; - rc = 1; + sqlite3_finalize(pStmt); +#ifndef SQLITE_OMIT_INTROSPECTION_PRAGMAS + if( zName ){ + appendText(&sSelect, + " UNION ALL SELECT shell_module_schema(name)," + " 'table', name, name, name, 9e+99, 'main'" + " FROM pragma_module_list", + 0); + } +#endif + appendText(&sSelect, ") WHERE ", 0); + if( zName ){ + char *zQarg = smprintf("%Q", zName); + int bGlob; + shell_check_oom(zQarg); + bGlob = strchr(zName, '*') != 0 || strchr(zName, '?') != 0 + || strchr(zName, '[') != 0; + if( strchr(zName, '.') ){ + appendText(&sSelect, "lower(printf('%s.%s',sname,tbl_name))", 0); }else{ - if( zBOM[0] ) fwrite(zBOM, 1, 3, p->out); - sqlite3_snprintf(sizeof(p->outfile), p->outfile, "%s", zFile); + appendText(&sSelect, "lower(tbl_name)", 0); } -#endif - }else{ - p->out = output_file_open(zFile, bTxtMode); - if( p->out==0 ){ - if( strcmp(zFile,"off")!=0 ){ - utf8_printf(stderr,"Error: cannot write to \"%s\"\n", zFile); - } - p->out = stdout; - rc = 1; - } else { - if( zBOM[0] ) fwrite(zBOM, 1, 3, p->out); - sqlite3_snprintf(sizeof(p->outfile), p->outfile, "%s", zFile); + appendText(&sSelect, bGlob ? " GLOB " : " LIKE ", 0); + appendText(&sSelect, zQarg, 0); + if( !bGlob ){ + appendText(&sSelect, " ESCAPE '\\' ", 0); } + appendText(&sSelect, " AND ", 0); + sqlite3_free(zQarg); } - sqlite3_free(zFile); - }else - - if( c=='p' && n>=3 && strncmp(azArg[0], "parameter", n)==0 ){ - open_db(p,0); - if( nArg<=1 ) goto parameter_syntax_error; - - /* .parameter clear - ** Clear all bind parameters by dropping the TEMP table that holds them. - */ - if( nArg==2 && strcmp(azArg[1],"clear")==0 ){ - sqlite3_exec(p->db, "DROP TABLE IF EXISTS temp.sqlite_parameters;", - 0, 0, 0); - }else - - /* .parameter list - ** List all bind parameters. - */ - if( nArg==2 && strcmp(azArg[1],"list")==0 ){ - sqlite3_stmt *pStmt = 0; - int rx; - int len = 0; - rx = sqlite3_prepare_v2(p->db, - "SELECT max(length(key)) " - "FROM temp.sqlite_parameters;", -1, &pStmt, 0); - if( rx==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ - len = sqlite3_column_int(pStmt, 0); - if( len>40 ) len = 40; - } - sqlite3_finalize(pStmt); - pStmt = 0; - if( len ){ - rx = sqlite3_prepare_v2(p->db, - "SELECT key, quote(value) " - "FROM temp.sqlite_parameters;", -1, &pStmt, 0); - while( rx==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ - utf8_printf(p->out, "%-*s %s\n", len, sqlite3_column_text(pStmt,0), - sqlite3_column_text(pStmt,1)); - } - sqlite3_finalize(pStmt); - } - }else - - /* .parameter init - ** Make sure the TEMP table used to hold bind parameters exists. - ** Create it if necessary. - */ - if( nArg==2 && strcmp(azArg[1],"init")==0 ){ - bind_table_init(p); - }else - - /* .parameter set NAME VALUE - ** Set or reset a bind parameter. NAME should be the full parameter - ** name exactly as it appears in the query. (ex: $abc, @def). The - ** VALUE can be in either SQL literal notation, or if not it will be - ** understood to be a text string. - */ - if( nArg==4 && strcmp(azArg[1],"set")==0 ){ - int rx; - char *zSql; - sqlite3_stmt *pStmt; - const char *zKey = azArg[2]; - const char *zValue = azArg[3]; - bind_table_init(p); - zSql = sqlite3_mprintf( - "REPLACE INTO temp.sqlite_parameters(key,value)" - "VALUES(%Q,%s);", zKey, zValue); - shell_check_oom(zSql); - pStmt = 0; - rx = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - if( rx!=SQLITE_OK ){ - sqlite3_finalize(pStmt); - pStmt = 0; - zSql = sqlite3_mprintf( - "REPLACE INTO temp.sqlite_parameters(key,value)" - "VALUES(%Q,%Q);", zKey, zValue); - shell_check_oom(zSql); - rx = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - if( rx!=SQLITE_OK ){ - utf8_printf(p->out, "Error: %s\n", sqlite3_errmsg(p->db)); - sqlite3_finalize(pStmt); - pStmt = 0; - rc = 1; - } - } - sqlite3_step(pStmt); - sqlite3_finalize(pStmt); - }else + if( bNoSystemTabs ){ + appendText(&sSelect, "name NOT LIKE 'sqlite_%%' AND ", 0); + } + appendText(&sSelect, "sql IS NOT NULL" + " ORDER BY snum, rowid", 0); + if( bDebug ){ + utf8_printf(data.out, "SQL: %s;\n", sSelect.z); + }else{ + rc = sqlite3_exec(datax.dbUser, sSelect.z, callback, &datax, &zErrMsg); + } + freeText(&sSelect); + } + if( zErrMsg ){ + *pzErr = zErrMsg; + return DCR_Error; + }else if( rc != SQLITE_OK ){ + *pzErr = smprintf("Error: querying schema information\n"); + return DCR_Error; + }else{ + return DCR_Ok; + } +} - /* .parameter unset NAME - ** Remove the NAME binding from the parameter binding table, if it - ** exists. - */ - if( nArg==3 && strcmp(azArg[1],"unset")==0 ){ - char *zSql = sqlite3_mprintf( - "DELETE FROM temp.sqlite_parameters WHERE key=%Q", azArg[2]); - shell_check_oom(zSql); - sqlite3_exec(p->db, zSql, 0, 0, 0); - sqlite3_free(zSql); - }else - /* If no command name matches, show a syntax error */ - parameter_syntax_error: - showHelp(p->out, "parameter"); - }else +/***************** + * The .selecttrace, .separator, .session and .sha3sum commands + */ +CONDITION_COMMAND( session defined(SQLITE_ENABLE_SESSION) ); +COLLECT_HELP_TEXT[ + ".separator COL ?ROW? Change the column and row separators", + ".session ?NAME? CMD ... Create or control sessions", + " Subcommands:", + " attach TABLE Attach TABLE", + " changeset FILE Write a changeset into FILE", + " close Close one session", + " enable ?BOOLEAN? Set or query the enable bit", + " filter GLOB... Reject tables matching GLOBs", + " indirect ?BOOLEAN? Mark or query the indirect status", + " isempty Query whether the session is empty", + " list List currently open session names", + " open DB NAME Open a new session on DB", + " patchset FILE Write a patchset into FILE", + " If ?NAME? is omitted, the first defined session is used.", + ".sha3sum ... Compute a SHA3 hash of database content", + " Options:", + " --schema Also hash the sqlite_schema table", + " --sha3-224 Use the sha3-224 algorithm", + " --sha3-256 Use the sha3-256 algorithm (default)", + " --sha3-384 Use the sha3-384 algorithm", + " --sha3-512 Use the sha3-512 algorithm", + " Any other argument is a LIKE pattern for tables to hash", +]; +DISPATCHABLE_COMMAND( selecttrace ? 1 0 ){ + unsigned int x = nArg>=2 ? (unsigned int)integerValue(azArg[1]) : 0xffffffff; + sqlite3_test_control(SQLITE_TESTCTRL_TRACEFLAGS, 1, &x); + return DCR_Ok; +} +DISPATCHABLE_COMMAND( separator ? 2 3 ){ + if( nArg>=2 ){ + sqlite3_snprintf(sizeof(ISS(p)->colSeparator), ISS(p)->colSeparator, + "%.*s", (int)ArraySize(ISS(p)->colSeparator)-1, azArg[1]); + } + if( nArg>=3 ){ + sqlite3_snprintf(sizeof(ISS(p)->rowSeparator), ISS(p)->rowSeparator, + "%.*s", (int)ArraySize(ISS(p)->rowSeparator)-1, azArg[2]); + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( session 3 2 0 ){ + int rc = 0; + struct AuxDb *pAuxDb = ISS(p)->pAuxDb; + OpenSession *pSession = &pAuxDb->aSession[0]; + FILE *out = ISS(p)->out; + char **azCmd = &azArg[1]; + int iSes = 0; + int nCmd = nArg - 1; + int i; + open_db(p, 0); + if( nArg>=3 ){ + for(iSes=0; iSesnSession; iSes++){ + if( strcmp(pAuxDb->aSession[iSes].zName, azArg[1])==0 ) break; + } + if( iSesnSession ){ + pSession = &pAuxDb->aSession[iSes]; + azCmd++; + nCmd--; + }else{ + pSession = &pAuxDb->aSession[0]; + iSes = 0; + } + } - if( c=='p' && n>=3 && strncmp(azArg[0], "print", n)==0 ){ - int i; - for(i=1; i1 ) raw_printf(p->out, " "); - utf8_printf(p->out, "%s", azArg[i]); + /* .session attach TABLE + ** Invoke the sqlite3session_attach() interface to attach a particular + ** table so that it is never filtered. + */ + if( strcmp(azCmd[0],"attach")==0 ){ + if( nCmd!=2 ) goto session_syntax_error; + if( pSession->p==0 ){ + session_not_open: + raw_printf(STD_ERR, "ERROR: No sessions are open\n"); + }else{ + rc = sqlite3session_attach(pSession->p, azCmd[1]); + if( rc ){ + raw_printf(STD_ERR, "ERROR: sqlite3session_attach() returns %d\n", rc); + rc = 0; + } } - raw_printf(p->out, "\n"); }else -#ifndef SQLITE_OMIT_PROGRESS_CALLBACK - if( c=='p' && n>=3 && strncmp(azArg[0], "progress", n)==0 ){ - int i; - int nn = 0; - p->flgProgress = 0; - p->mxProgress = 0; - p->nProgress = 0; - for(i=1; iflgProgress |= SHELL_PROGRESS_QUIET; - continue; - } - if( strcmp(z,"reset")==0 ){ - p->flgProgress |= SHELL_PROGRESS_RESET; - continue; + /* .session changeset FILE + ** .session patchset FILE + ** Write a changeset or patchset into a file. The file is overwritten. + */ + if( strcmp(azCmd[0],"changeset")==0 || strcmp(azCmd[0],"patchset")==0 ){ + FILE *cs_out = 0; + if( failIfSafeMode + (p, "cannot run \".session %s\" in safe mode", azCmd[0]) ){ + rc = DCR_AbortError; + }else{ + if( nCmd!=2 ) goto session_syntax_error; + if( pSession->p==0 ) goto session_not_open; + cs_out = fopen(azCmd[1], "wb"); + if( cs_out==0 ){ + *pzErr = smprintf("cannot open \"%s\" for writing\n", azCmd[1]); + rc = 1; + }else{ + int szChng; + void *pChng; + if( azCmd[0][0]=='c' ){ + rc = sqlite3session_changeset(pSession->p, &szChng, &pChng); + }else{ + rc = sqlite3session_patchset(pSession->p, &szChng, &pChng); } - if( strcmp(z,"once")==0 ){ - p->flgProgress |= SHELL_PROGRESS_ONCE; - continue; + if( rc ){ + fprintf(out, "Error: error code %d\n", rc); + rc = 0; } - if( strcmp(z,"limit")==0 ){ - if( i+1>=nArg ){ - utf8_printf(stderr, "Error: missing argument on --limit\n"); - rc = 1; - goto meta_command_exit; - }else{ - p->mxProgress = (int)integerValue(azArg[++i]); - } - continue; + if( pChng && fwrite(pChng, szChng, 1, cs_out)!=1 ){ + raw_printf(STD_ERR, "ERROR: Failed to write entire %d-byte output\n", + szChng); } - utf8_printf(stderr, "Error: unknown option: \"%s\"\n", azArg[i]); - rc = 1; - goto meta_command_exit; - }else{ - nn = (int)integerValue(z); + sqlite3_free(pChng); + fclose(cs_out); } } - open_db(p, 0); - sqlite3_progress_handler(p->db, nn, progress_handler, p); }else -#endif /* SQLITE_OMIT_PROGRESS_CALLBACK */ - if( c=='p' && strncmp(azArg[0], "prompt", n)==0 ){ - if( nArg >= 2) { - strncpy(mainPrompt,azArg[1],(int)ArraySize(mainPrompt)-1); - } - if( nArg >= 3) { - strncpy(continuePrompt,azArg[2],(int)ArraySize(continuePrompt)-1); + /* .session close + ** Close the identified session + */ + if( strcmp(azCmd[0], "close")==0 ){ + if( nCmd!=1 ) goto session_syntax_error; + if( pAuxDb->nSession ){ + session_close(pSession); + pAuxDb->aSession[iSes] = pAuxDb->aSession[--pAuxDb->nSession]; } }else - if( c=='q' && strncmp(azArg[0], "quit", n)==0 ){ - rc = 2; + /* .session enable ?BOOLEAN? + ** Query or set the enable flag + */ + if( strcmp(azCmd[0], "enable")==0 ){ + int ii; + if( nCmd>2 ) goto session_syntax_error; + ii = nCmd==1 ? -1 : booleanValue(azCmd[1]); + if( pAuxDb->nSession ){ + ii = sqlite3session_enable(pSession->p, ii); + utf8_printf(out, "session %s enable flag = %d\n", + pSession->zName, ii); + } }else - if( c=='r' && n>=3 && strncmp(azArg[0], "read", n)==0 ){ - FILE *inSaved = p->in; - int savedLineno = p->lineno; - failIfSafeMode(p, "cannot run .read in safe mode"); - if( nArg!=2 ){ - raw_printf(stderr, "Usage: .read FILE\n"); - rc = 1; - goto meta_command_exit; - } - if( azArg[1][0]=='|' ){ -#ifdef SQLITE_OMIT_POPEN - raw_printf(stderr, "Error: pipes are not supported in this OS\n"); - rc = 1; - p->out = stdout; -#else - p->in = popen(azArg[1]+1, "r"); - if( p->in==0 ){ - utf8_printf(stderr, "Error: cannot open \"%s\"\n", azArg[1]); - rc = 1; - }else{ - rc = process_input(p); - pclose(p->in); + /* .session filter GLOB .... + ** Set a list of GLOB patterns of table names to be excluded. + */ + if( strcmp(azCmd[0], "filter")==0 ){ + int ii, nByte; + if( nCmd<2 ) goto session_syntax_error; + if( pAuxDb->nSession ){ + for(ii=0; iinFilter; ii++){ + sqlite3_free(pSession->azFilter[ii]); } -#endif - }else if( (p->in = openChrSource(azArg[1]))==0 ){ - utf8_printf(stderr,"Error: cannot open \"%s\"\n", azArg[1]); - rc = 1; - }else{ - rc = process_input(p); - fclose(p->in); + sqlite3_free(pSession->azFilter); + nByte = sizeof(pSession->azFilter[0])*(nCmd-1); + pSession->azFilter = sqlite3_malloc( nByte ); + if( pSession->azFilter==0 ){ + shell_out_of_memory(); + } + for(ii=1; iiazFilter[ii-1] = smprintf("%s", azCmd[ii]); + shell_check_oom(pSession->azFilter[ii-1]); + } + pSession->nFilter = ii-1; } - p->in = inSaved; - p->lineno = savedLineno; }else - if( c=='r' && n>=3 && strncmp(azArg[0], "restore", n)==0 ){ - const char *zSrcFile; - const char *zDb; - sqlite3 *pSrc; - sqlite3_backup *pBackup; - int nTimeout = 0; - - failIfSafeMode(p, "cannot run .restore in safe mode"); - if( nArg==2 ){ - zSrcFile = azArg[1]; - zDb = "main"; - }else if( nArg==3 ){ - zSrcFile = azArg[2]; - zDb = azArg[1]; - }else{ - raw_printf(stderr, "Usage: .restore ?DB? FILE\n"); - rc = 1; - goto meta_command_exit; - } - rc = sqlite3_open(zSrcFile, &pSrc); - if( rc!=SQLITE_OK ){ - utf8_printf(stderr, "Error: cannot open \"%s\"\n", zSrcFile); - close_db(pSrc); - return 1; - } - open_db(p, 0); - pBackup = sqlite3_backup_init(p->db, zDb, pSrc, "main"); - if( pBackup==0 ){ - utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db)); - close_db(pSrc); - return 1; + /* .session indirect ?BOOLEAN? + ** Query or set the indirect flag + */ + if( strcmp(azCmd[0], "indirect")==0 ){ + int ii; + if( nCmd>2 ) goto session_syntax_error; + ii = nCmd==1 ? -1 : booleanValue(azCmd[1]); + if( pAuxDb->nSession ){ + ii = sqlite3session_indirect(pSession->p, ii); + utf8_printf(out, "session %s indirect flag = %d\n", + pSession->zName, ii); } - while( (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK - || rc==SQLITE_BUSY ){ - if( rc==SQLITE_BUSY ){ - if( nTimeout++ >= 3 ) break; - sqlite3_sleep(100); - } + }else + + /* .session isempty + ** Determine if the session is empty + */ + if( strcmp(azCmd[0], "isempty")==0 ){ + int ii; + if( nCmd!=1 ) goto session_syntax_error; + if( pAuxDb->nSession ){ + ii = sqlite3session_isempty(pSession->p); + utf8_printf(out, "session %s isempty flag = %d\n", + pSession->zName, ii); } - sqlite3_backup_finish(pBackup); - if( rc==SQLITE_DONE ){ - rc = 0; - }else if( rc==SQLITE_BUSY || rc==SQLITE_LOCKED ){ - raw_printf(stderr, "Error: source database is busy\n"); - rc = 1; - }else{ - utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db)); - rc = 1; + }else + + /* .session list + ** List all currently open sessions + */ + if( strcmp(azCmd[0],"list")==0 ){ + for(i=0; inSession; i++){ + utf8_printf(out, "%d %s\n", i, pAuxDb->aSession[i].zName); } - close_db(pSrc); }else - if( c=='s' && strncmp(azArg[0], "scanstats", n)==0 ){ - if( nArg==2 ){ - p->scanstatsOn = (u8)booleanValue(azArg[1]); -#ifndef SQLITE_ENABLE_STMT_SCANSTATUS - raw_printf(stderr, "Warning: .scanstats not available in this build.\n"); -#endif + /* .session open DB NAME + ** Open a new session called NAME on the attached database DB. + ** DB is normally "main". + */ + if( strcmp(azCmd[0],"open")==0 ){ + char *zName; + if( nCmd!=3 ) goto session_syntax_error; + zName = azCmd[2]; + if( zName[0]==0 ) goto session_syntax_error; + for(i=0; inSession; i++){ + if( strcmp(pAuxDb->aSession[i].zName,zName)==0 ){ + utf8_printf(STD_ERR, "Session \"%s\" already exists\n", zName); + return rc; + } + } + if( pAuxDb->nSession>=ArraySize(pAuxDb->aSession) ){ + raw_printf + (STD_ERR, "Maximum of %d sessions\n", ArraySize(pAuxDb->aSession)); + return rc; + } + pSession = &pAuxDb->aSession[pAuxDb->nSession]; + rc = sqlite3session_create(DBX(p), azCmd[1], &pSession->p); + if( rc ){ + *pzErr = smprintf("Cannot open session: error code=%d\n", rc); + return rc; + } + pSession->nFilter = 0; + sqlite3session_table_filter(pSession->p, session_filter, pSession); + pAuxDb->nSession++; + shell_newstr_assign(&pSession->zName, smprintf("%s", zName)); + }else{ + + /* If no command name matches, show a syntax error */ + session_syntax_error: + showHelp(out, "session", p); + return DCR_CmdErred; + } + return rc; +} +DISPATCHABLE_COMMAND( sha3sum 4 1 1 ){ + const char *zLike = 0; /* Which table to checksum. 0 means everything */ + int i; /* Loop counter */ + int bSchema = 0; /* Also hash the schema */ + int bSeparate = 0; /* Hash each table separately */ + int iSize = 224; /* Hash algorithm to use */ + int bDebug = 0; /* Only show the query that would have run */ + sqlite3_stmt *pStmt; /* For querying tables names */ + char *zSql; /* SQL to be run */ + char *zSep; /* Separator */ + ShellText sSql; /* Complete SQL for the query to run the hash */ + ShellText sQuery; /* Set of queries used to read all content */ + open_db(p, 0); + for(i=1; i1" + " UNION ALL SELECT 'sqlite_schema'" + " ORDER BY 1 collate nocase"; + }else{ + zSql = "SELECT lower(name) FROM sqlite_schema" + " WHERE type='table' AND coalesce(rootpage,0)>1" + " AND name NOT LIKE 'sqlite_%'" + " ORDER BY 1 collate nocase"; + } + sqlite3_prepare_v2(DBX(p), zSql, -1, &pStmt, 0); + initText(&sQuery); + initText(&sSql); + appendText(&sSql, "WITH [sha3sum$query](a,b) AS(",0); + zSep = "VALUES("; + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *zTab = (const char*)sqlite3_column_text(pStmt,0); + if( zTab==0 ) continue; + if( zLike && sqlite3_strlike(zLike, zTab, 0)!=0 ) continue; + if( strncmp(zTab, "sqlite_",7)!=0 ){ + appendText(&sQuery,"SELECT * FROM ", 0); + appendText(&sQuery,zTab,'"'); + appendText(&sQuery," NOT INDEXED;", 0); + }else if( strcmp(zTab, "sqlite_schema")==0 ){ + appendText(&sQuery,"SELECT type,name,tbl_name,sql FROM sqlite_schema" + " ORDER BY name;", 0); + }else if( strcmp(zTab, "sqlite_sequence")==0 ){ + appendText(&sQuery,"SELECT name,seq FROM sqlite_sequence" + " ORDER BY name;", 0); + }else if( strcmp(zTab, "sqlite_stat1")==0 ){ + appendText(&sQuery,"SELECT tbl,idx,stat FROM sqlite_stat1" + " ORDER BY tbl,idx;", 0); + }else if( strcmp(zTab, "sqlite_stat4")==0 ){ + appendText(&sQuery, "SELECT * FROM ", 0); + appendText(&sQuery, zTab, 0); + appendText(&sQuery, " ORDER BY tbl, idx, rowid;\n", 0); + } + appendText(&sSql, zSep, 0); + appendText(&sSql, sQuery.z, '\''); + sQuery.n = 0; + appendText(&sSql, ",", 0); + appendText(&sSql, zTab, '\''); + zSep = "),("; + } + sqlite3_finalize(pStmt); + if( bSeparate ){ + zSql = smprintf( + "%s))" + " SELECT lower(hex(sha3_query(a,%d))) AS hash, b AS label" + " FROM [sha3sum$query]", + sSql.z, iSize); + }else{ + zSql = smprintf( + "%s))" + " SELECT lower(hex(sha3_query(group_concat(a,''),%d))) AS hash" + " FROM [sha3sum$query]", + sSql.z, iSize); + } + shell_check_oom(zSql); + freeText(&sQuery); + freeText(&sSql); + if( bDebug ){ + utf8_printf(ISS(p)->out, "%s\n", zSql); + }else{ + shell_exec(p, zSql, 0); + } + sqlite3_free(zSql); + return DCR_Ok; +} - if( c=='s' && strncmp(azArg[0], "schema", n)==0 ){ - ShellText sSelect; - ShellState data; - char *zErrMsg = 0; - const char *zDiv = "("; - const char *zName = 0; - int iSchema = 0; - int bDebug = 0; - int bNoSystemTabs = 0; - int ii; +/***************** + * The .selftest*, .shell, and .show commands + */ +CONDITION_COMMAND( selftest_bool defined(SQLITE_DEBUG) ); +CONDITION_COMMAND( selftest_int defined(SQLITE_DEBUG) ); +COLLECT_HELP_TEXT[ + ",selftest ?OPTIONS? Run tests defined in the SELFTEST table", + " Options:", + " --init Create a new SELFTEST table", + " -v Verbose output", + ",selftest_bool ?ARGS? Show boolean values of ARGS as flag tokens", + ",selftest_int ?ARGS? Show integer values of ARGS as integer tokens", + ".show Show the current values for various settings", +]; - open_db(p, 0); - memcpy(&data, p, sizeof(data)); - data.showHeader = 0; - data.cMode = data.mode = MODE_Semi; - initText(&sSelect); - for(ii=1; iidb, "SELECT name FROM pragma_database_list", - -1, &pStmt, 0); - if( rc ){ - utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db)); - sqlite3_finalize(pStmt); - rc = 1; - goto meta_command_exit; - } - appendText(&sSelect, "SELECT sql FROM", 0); - iSchema = 0; - while( sqlite3_step(pStmt)==SQLITE_ROW ){ - const char *zDb = (const char*)sqlite3_column_text(pStmt, 0); - char zScNum[30]; - sqlite3_snprintf(sizeof(zScNum), zScNum, "%d", ++iSchema); - appendText(&sSelect, zDiv, 0); - zDiv = " UNION ALL "; - appendText(&sSelect, "SELECT shell_add_schema(sql,", 0); - if( sqlite3_stricmp(zDb, "main")!=0 ){ - appendText(&sSelect, zDb, '\''); - }else{ - appendText(&sSelect, "NULL", 0); +DISPATCHABLE_COMMAND( selftest_bool 10 0 0 ){ + int i, v; + for(i=1; iout, "%s: %d 0x%x\n", azArg[i], v, v); + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( selftest_int 10 0 0 ){ + int i; sqlite3_int64 v; + for(i=1; iout, "%s", zBuf); + } + return DCR_Ok; +} + +DISPATCHABLE_COMMAND( selftest 4 0 0 ){ - int rc; ++ int rc = 0; + ShellInState *psi = ISS(p); + int bIsInit = 0; /* True to initialize the SELFTEST table */ + int bVerbose = 0; /* Verbose output */ + int bSelftestExists; /* True if SELFTEST already exists */ + int i, k; /* Loop counters */ + int nTest = 0; /* Number of tests runs */ + int nErr = 0; /* Number of errors seen */ + ShellText str; /* Answer for a query */ + sqlite3_stmt *pStmt = 0; /* Query against the SELFTEST table */ + + for(i=1; i=0; k--){ + if( k==1 ){ + rc = sqlite3_prepare_v2(DBX(p), + "SELECT tno,op,cmd,ans FROM selftest ORDER BY tno", + -1, &pStmt, 0); + }else{ + rc = sqlite3_prepare_v2(DBX(p), + "VALUES(0,'memo','Missing SELFTEST table - default checks only','')," + " (1,'run','PRAGMA integrity_check','ok')", + -1, &pStmt, 0); + } + if( rc ){ + *pzErr = smprintf("Error querying the selftest table\n"); sqlite3_finalize(pStmt); -#ifndef SQLITE_OMIT_INTROSPECTION_PRAGMAS - if( zName ){ - appendText(&sSelect, - " UNION ALL SELECT shell_module_schema(name)," - " 'table', name, name, name, 9e+99, 'main' FROM pragma_module_list", - 0); - } -#endif - appendText(&sSelect, ") WHERE ", 0); - if( zName ){ - char *zQarg = sqlite3_mprintf("%Q", zName); - int bGlob; - shell_check_oom(zQarg); - bGlob = strchr(zName, '*') != 0 || strchr(zName, '?') != 0 || - strchr(zName, '[') != 0; - if( strchr(zName, '.') ){ - appendText(&sSelect, "lower(printf('%s.%s',sname,tbl_name))", 0); - }else{ - appendText(&sSelect, "lower(tbl_name)", 0); + return DCR_Error; + } + for(i=1; sqlite3_step(pStmt)==SQLITE_ROW; i++){ + int tno = sqlite3_column_int(pStmt, 0); + const char *zOp = (const char*)sqlite3_column_text(pStmt, 1); + const char *zSql = (const char*)sqlite3_column_text(pStmt, 2); + const char *zAns = (const char*)sqlite3_column_text(pStmt, 3); + + if( zOp==0 || zSql==0 || zAns==0 ) continue; + k = 0; + if( bVerbose>0 ){ + /* This unusually directed output is for test purposes. */ + fprintf(STD_OUT, "%d: %s %s\n", tno, zOp, zSql); + } + if( strcmp(zOp,"memo")==0 ){ + utf8_printf(psi->out, "%s\n", zSql); + }else if( strcmp(zOp,"run")==0 ){ + char *zErrMsg = 0; + str.n = 0; + str.z[0] = 0; + rc = sqlite3_exec(DBX(p), zSql, captureOutputCallback, &str, &zErrMsg); + nTest++; + if( bVerbose ){ + utf8_printf(psi->out, "Result: %s\n", str.z); } - appendText(&sSelect, bGlob ? " GLOB " : " LIKE ", 0); - appendText(&sSelect, zQarg, 0); - if( !bGlob ){ - appendText(&sSelect, " ESCAPE '\\' ", 0); + if( rc || zErrMsg ){ + nErr++; + rc = 1; + utf8_printf(psi->out, "%d: error-code-%d: %s\n", tno, rc, zErrMsg); + sqlite3_free(zErrMsg); + }else if( strcmp(zAns,str.z)!=0 ){ + nErr++; + rc = 1; + utf8_printf(psi->out, "%d: Expected: [%s]\n", tno, zAns); + utf8_printf(psi->out, "%d: Got: [%s]\n", tno, str.z); } - appendText(&sSelect, " AND ", 0); - sqlite3_free(zQarg); + }else{ + *pzErr = smprintf + ("Unknown operation \"%s\" on selftest line %d\n", zOp, tno); + rc = 1; + break; } - if( bNoSystemTabs ){ - appendText(&sSelect, "name NOT LIKE 'sqlite_%%' AND ", 0); + } /* End loop over rows of content from SELFTEST */ + sqlite3_finalize(pStmt); + } /* End loop over k */ + freeText(&str); + utf8_printf(psi->out, "%d errors out of %d tests\n", nErr, nTest); + return rc > 0; +} + +/***************** + * The .shell and .system commands + */ +CONDITION_COMMAND( shell !defined(SQLITE_NOHAVE_SYSTEM) ); +CONDITION_COMMAND( system !defined(SQLITE_NOHAVE_SYSTEM) ); +COLLECT_HELP_TEXT[ + ".shell CMD ARGS... Run CMD ARGS... in a system shell", + ".system CMD ARGS... Run CMD ARGS... in a system shell", +]; + +#ifndef SQLITE_NOHAVE_SYSTEM +static DotCmdRC shellOut(char *azArg[], int nArg, + ShellExState *psx, char **pzErr){ + char *zCmd; + int i, x; + if( ISS(psx)->bSafeMode ) return DCR_AbortError; + zCmd = smprintf(strchr(azArg[1],' ')==0?"%s":"\"%s\"", azArg[1]); + for(i=2; ibSafeMode ) return DCR_AbortError; + while( ai1 ){ + for( ia=1; iabExtendedDotCmds |= shopts[io].mask; + else psi->bExtendedDotCmds &= ~shopts[io].mask; + break; + } } - appendText(&sSelect, "sql IS NOT NULL" - " ORDER BY snum, rowid", 0); - if( bDebug ){ - utf8_printf(p->out, "SQL: %s;\n", sSelect.z); - }else{ - rc = sqlite3_exec(p->db, sSelect.z, callback, &data, &zErrMsg); + if( io==ArraySize(shopts) ){ + zAbout = azArg[ia]; + zMoan = "is not a recognized option name"; + goto moan_error; } - freeText(&sSelect); } - if( zErrMsg ){ - utf8_printf(stderr,"Error: %s\n", zErrMsg); - sqlite3_free(zErrMsg); - rc = 1; - }else if( rc != SQLITE_OK ){ - raw_printf(stderr,"Error: querying schema information\n"); - rc = 1; + }else{ + raw_printf(psi->out, + " name value \"-shxopts set\"\n" + " -------- ----- ---------------\n"); + for( io=0; iobExtendedDotCmds & m) == m)? 1 : 0; + raw_printf(psi->out, + " %9s %2d \"-shxopts 0x%02X\"\n", + shopts[io].name, v, m); + } + } + return DCR_Ok; + moan_error: + raw_printf(STD_ERR, "Error: %s %s\n", zAbout, zMoan); + return DCR_CmdErred; +} +DISPATCHABLE_COMMAND( show ? 1 1 ){ + static const char *azBool[] = { "off", "on", "trigger", "full"}; + const char *zOut; + ShellInState *psi = ISS(p); + FILE *out = psi->out; + int i; + utf8_printf(out, "%12.12s: %s\n","echo", + azBool[ShellHasFlag(p, SHFLG_Echo)]); + utf8_printf(out, "%12.12s: %s\n","eqp", azBool[psi->autoEQP&3]); + utf8_printf(out, "%12.12s: %s\n","explain", + psi->mode==MODE_Explain + ? "on" : psi->autoExplain ? "auto" : "off"); + utf8_printf(out,"%12.12s: %s\n","headers", azBool[psi->showHeader!=0]); + zOut = modeDescr[psi->mode].zModeName; + i = strlen30(zOut) - modeDescr[psi->mode].bDepluralize; + if( MODE_IS_COLUMNAR(psi->mode) ){ + utf8_printf + (out, "%12.12s: %.*s --wrap %d --wordwrap %s --%squote\n", "mode", + i, zOut, psi->cmOpts.iWrap, + psi->cmOpts.bWordWrap ? "on" : "off", + psi->cmOpts.bQuote ? "" : "no"); + }else{ + utf8_printf(out, "%12.12s: %.*s\n","mode", i, zOut); + } + utf8_printf(out, "%12.12s: ", "nullvalue"); + output_c_string(out, psi->nullValue); + raw_printf(out, "\n"); + utf8_printf(out,"%12.12s: %s\n","output", + strlen30(psi->outfile) ? psi->outfile : "stdout"); + utf8_printf(out,"%12.12s: ", "colseparator"); + output_c_string(out, psi->colSeparator); + raw_printf(out, "\n"); + utf8_printf(out,"%12.12s: ", "rowseparator"); + output_c_string(out, psi->rowSeparator); + raw_printf(out, "\n"); + switch( psi->statsOn ){ + case 0: zOut = "off"; break; + default: zOut = "on"; break; + case 2: zOut = "stmt"; break; + case 3: zOut = "vmstep"; break; + } + utf8_printf(out, "%12.12s: %s\n","stats", zOut); + utf8_printf(out, "%12.12s: ", "width"); + for (i=0;inumWidths;i++) { + raw_printf(out, "%d ", p->pSpecWidths[i]); + } + raw_printf(out, "\n"); + utf8_printf(out, "%12.12s: %s\n", "filename", + psi->pAuxDb->zDbFilename ? psi->pAuxDb->zDbFilename : ""); + return DCR_Ok; +} + +/***************** + * The .stats command + */ +COLLECT_HELP_TEXT[ + ".stats ?ARG? Show stats or turn stats on or off", + " off Turn off automatic stat display", + " on Turn on automatic stat display", + " stmt Show statement stats", + " vmstep Show the virtual machine step count only", +]; +DISPATCHABLE_COMMAND( stats ? 0 0 ){ + ShellInState *psi = ISS(p); + if( nArg==2 ){ + if( strcmp(azArg[1],"stmt")==0 ){ + psi->statsOn = 2; + }else if( strcmp(azArg[1],"vmstep")==0 ){ + psi->statsOn = 3; }else{ - rc = 0; + psi->statsOn = (u8)booleanValue(azArg[1]); } - }else + }else if( nArg==1 ){ + display_stats(DBX(p), psi, 0); + }else{ + *pzErr = smprintf("Usage: .stats ?on|off|stmt|vmstep?\n"); + return DCR_SayUsage; + } + return DCR_Ok; +} + +/***************** + * The .tables, .views, .indices and .indexes command + * These are together because they share implementation or are aliases. + */ +COLLECT_HELP_TEXT[ + ".indexes ?TABLE? Show names of indexes", + " If TABLE is specified, only show indexes for", + " tables matching TABLE using the LIKE operator.", +]; +static int showTableLike(char *azArg[], int nArg, ShellExState *p, + char **pzErr, char ot){ + int rc; + sqlite3_stmt *pStmt; + char **azResult; + int nRow, nAlloc; + int ii; + ShellText s; + initText(&s); + open_db(p, 0); + rc = sqlite3_prepare_v2(DBX(p), "PRAGMA database_list", -1, &pStmt, 0); + if( rc ){ + sqlite3_finalize(pStmt); + return shellDatabaseError(DBX(p)); + } + + if( nArg>2 && ot=='i' ){ + /* It is an historical accident that the .indexes command shows an error + ** when called with the wrong number of arguments whereas the .tables + ** command does not. */ + *pzErr = smprintf("Usage: .indexes ?LIKE-PATTERN?\n"); + sqlite3_finalize(pStmt); + return DCR_SayUsage; + } + for(ii=0; sqlite3_step(pStmt)==SQLITE_ROW; ii++){ + const char *zDbName = (const char*)sqlite3_column_text(pStmt, 1); + const char *zFilter = ""; + const char *zSystem = " AND name NOT LIKE 'sqlite_%'"; + if( zDbName==0 ) continue; + if( s.z && s.z[0] ) appendText(&s, " UNION ALL ", 0); + if( sqlite3_stricmp(zDbName, "main")==0 ){ + appendText(&s, "SELECT name FROM ", 0); + }else{ + appendText(&s, "SELECT ", 0); + appendText(&s, zDbName, '\''); + appendText(&s, "||'.'||name FROM ", 0); + } + appendText(&s, zDbName, '"'); + appendText(&s, ".sqlite_schema ", 0); + switch (ot) { + case 'i': + zFilter = "'index'"; + break; +#ifndef LEGACY_TABLES_LISTING + case 't': + zFilter = "'table'"; + break; + case 'v': + zFilter = "'view'"; + break; +#endif + case 's': + zSystem = " AND name LIKE 'sqlite_%'"; + /* fall thru */ + case 'T': + zFilter = "'table','view'"; + break; + default: + assert(0); + } + appendText(&s, " WHERE type IN(", 0); + appendText(&s, zFilter, 0); + appendText(&s, ") AND name LIKE ?1", 0); + appendText(&s, zSystem, 0); + } + rc = sqlite3_finalize(pStmt); + if( rc==SQLITE_OK ){ + appendText(&s, " ORDER BY 1", 0); + rc = sqlite3_prepare_v2(DBX(p), s.z, -1, &pStmt, 0); + } + freeText(&s); + if( rc ) return shellDatabaseError(DBX(p)); + + /* Run the SQL statement prepared by the above block. Store the results + ** as an array of nul-terminated strings in azResult[]. */ + nRow = nAlloc = 0; + azResult = 0; + if( nArg>1 ){ + sqlite3_bind_text(pStmt, 1, azArg[1], -1, SQLITE_TRANSIENT); + }else{ + sqlite3_bind_text(pStmt, 1, "%", -1, SQLITE_STATIC); + } + while( sqlite3_step(pStmt)==SQLITE_ROW ){ + if( nRow>=nAlloc ){ + char **azNew; + int n2 = nAlloc*2 + 10; + azNew = sqlite3_realloc64(azResult, sizeof(azResult[0])*n2); + shell_check_oom(azNew); + nAlloc = n2; + azResult = azNew; + } + azResult[nRow] = smprintf("%s", sqlite3_column_text(pStmt, 0)); + shell_check_oom(azResult[nRow]); + nRow++; + } + if( sqlite3_finalize(pStmt)!=SQLITE_OK ){ + rc = shellDatabaseError(DBX(p)); + } + + /* Pretty-print the contents of array azResult[] to the output */ + if( rc==0 && nRow>0 ){ + int len, maxlen = 0; + int i, j; + int nPrintCol, nPrintRow; + for(i=0; imaxlen ) maxlen = len; + } + nPrintCol = 80/(maxlen+2); + if( nPrintCol<1 ) nPrintCol = 1; + nPrintRow = (nRow + nPrintCol - 1)/nPrintCol; + for(i=0; iout, "%s%-*s", zSp, maxlen, + azResult[j] ? azResult[j]:""); + } + raw_printf(ISS(p)->out, "\n"); + } + } + + for(ii=0; ii1 && azArg[1][0]=='-' && azArg[1][1]!=0 && azArg[1][2]==0 ){ + char c = azArg[1][1]; + switch (c){ + case 's': + case 't': + case 'v': + objType = c; + ++azArg; + --nArg; + break; + default: + return DCR_Unknown|1; + } + } +#endif + return showTableLike(azArg, nArg, p, pzErr, objType); +} +DISPATCHABLE_COMMAND( indexes 3 1 2 ){ + return showTableLike(azArg, nArg, p, pzErr, 'i'); +} +DISPATCHABLE_COMMAND( indices 3 1 2 ){ + return showTableLike(azArg, nArg, p, pzErr, 'i'); +} - if( (c=='s' && n==11 && strncmp(azArg[0], "selecttrace", n)==0) - || (c=='t' && n==9 && strncmp(azArg[0], "treetrace", n)==0) - ){ - unsigned int x = nArg>=2 ? (unsigned int)integerValue(azArg[1]) : 0xffffffff; - sqlite3_test_control(SQLITE_TESTCTRL_TRACEFLAGS, 1, &x); - }else +/***************** + * The .testcase, .testctrl, .timeout, .timer and .trace commands + */ +CONDITION_COMMAND( testctrl !defined(SQLITE_UNTESTABLE) ); +CONDITION_COMMAND( trace !defined(SQLITE_OMIT_TRACE) ); +COLLECT_HELP_TEXT[ + ",testcase NAME Begin redirecting output to 'testcase-out.txt'", + ",testctrl CMD ... Run various sqlite3_test_control() operations", + " Run \".testctrl\" with no arguments for details", + ".timeout MS Try opening locked tables for MS milliseconds", + ".timer on|off Turn SQL timer on or off", + ".trace ?OPTIONS? Output each SQL statement as it is run", + " FILE Send output to FILE", + " stdout Send output to stdout", + " stderr Send output to stderr", + " off Disable tracing", + " --expanded Expand query parameters", +#ifdef SQLITE_ENABLE_NORMALIZE + " --normalized Normal the SQL statements", +#endif + " --plain Show SQL as it is input", + " --stmt Trace statement execution (SQLITE_TRACE_STMT)", + " --profile Profile statements (SQLITE_TRACE_PROFILE)", + " --row Trace each row (SQLITE_TRACE_ROW)", + " --close Trace connection close (SQLITE_TRACE_CLOSE)", +]; + +/* Begin redirecting output to the file "testcase-out.txt" */ +DISPATCHABLE_COMMAND( testcase ? 0 0 ){ + ShellInState *psi = ISS(p); + output_reset(psi); + psi->out = output_file_open("testcase-out.txt", 0); + if( psi->out==0 ){ + raw_printf(STD_ERR, "Error: cannot open 'testcase-out.txt'\n"); + } + if( nArg>=2 ){ + sqlite3_snprintf(sizeof(psi->zTestcase), psi->zTestcase, "%s", azArg[1]); + }else{ + sqlite3_snprintf(sizeof(psi->zTestcase), psi->zTestcase, "?"); + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( testctrl ? 0 0 ){ + FILE *out = ISS(p)->out; + static const struct { + const char *zCtrlName; /* Name of a test-control option */ + int ctrlCode; /* Integer code for that option */ + int unSafe; /* Not valid for --safe mode */ + const char *zUsage; /* Usage notes */ + } aCtrl[] = { + { "always", SQLITE_TESTCTRL_ALWAYS, 1, "BOOLEAN" }, + { "assert", SQLITE_TESTCTRL_ASSERT, 1, "BOOLEAN" }, + /*{ "benign_malloc_hooks",SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS,1, "" },*/ + /*{ "bitvec_test", SQLITE_TESTCTRL_BITVEC_TEST, 1, "" },*/ + { "byteorder", SQLITE_TESTCTRL_BYTEORDER, 0, "" }, + { "extra_schema_checks",SQLITE_TESTCTRL_EXTRA_SCHEMA_CHECKS,0,"BOOLEAN" }, + /*{ "fault_install", SQLITE_TESTCTRL_FAULT_INSTALL, 1,"" },*/ + { "imposter", SQLITE_TESTCTRL_IMPOSTER,1,"SCHEMA ON/OFF ROOTPAGE"}, + { "internal_functions", SQLITE_TESTCTRL_INTERNAL_FUNCTIONS,0,"" }, + { "localtime_fault", SQLITE_TESTCTRL_LOCALTIME_FAULT,0,"BOOLEAN" }, + { "never_corrupt", SQLITE_TESTCTRL_NEVER_CORRUPT,1, "BOOLEAN" }, + { "optimizations", SQLITE_TESTCTRL_OPTIMIZATIONS,0,"DISABLE-MASK" }, +#ifdef YYCOVERAGE + { "parser_coverage", SQLITE_TESTCTRL_PARSER_COVERAGE,0,"" }, +#endif + { "pending_byte", SQLITE_TESTCTRL_PENDING_BYTE,0, "OFFSET " }, + { "prng_restore", SQLITE_TESTCTRL_PRNG_RESTORE,0, "" }, + { "prng_save", SQLITE_TESTCTRL_PRNG_SAVE, 0, "" }, + { "prng_seed", SQLITE_TESTCTRL_PRNG_SEED, 0, "SEED ?db?" }, + { "seek_count", SQLITE_TESTCTRL_SEEK_COUNT, 0, "" }, + { "sorter_mmap", SQLITE_TESTCTRL_SORTER_MMAP, 0, "NMAX" }, + { "tune", SQLITE_TESTCTRL_TUNE, 1, "ID VALUE" }, + }; + int testctrl = -1; + int iCtrl = -1; + int rc2 = 0; /* 0: usage. 1: %d 2: %x 3: no-output */ + int isOk = 0; + int i, n2; + const char *zCmd = 0; -#if defined(SQLITE_ENABLE_SESSION) - if( c=='s' && strncmp(azArg[0],"session",n)==0 && n>=3 ){ - struct AuxDb *pAuxDb = p->pAuxDb; - OpenSession *pSession = &pAuxDb->aSession[0]; - char **azCmd = &azArg[1]; - int iSes = 0; - int nCmd = nArg - 1; - int i; - if( nArg<=1 ) goto session_syntax_error; - open_db(p, 0); - if( nArg>=3 ){ - for(iSes=0; iSesnSession; iSes++){ - if( strcmp(pAuxDb->aSession[iSes].zName, azArg[1])==0 ) break; - } - if( iSesnSession ){ - pSession = &pAuxDb->aSession[iSes]; - azCmd++; - nCmd--; - }else{ - pSession = &pAuxDb->aSession[0]; - iSes = 0; - } + open_db(p, 0); + zCmd = nArg>=2 ? azArg[1] : "help"; + + /* The argument can optionally begin with "-" or "--" */ + if( zCmd[0]=='-' && zCmd[1] ){ + zCmd++; + if( zCmd[0]=='-' && zCmd[1] ) zCmd++; + } + + /* --help lists all test-controls */ + if( strcmp(zCmd,"help")==0 ){ + utf8_printf(out, "Available test-controls:\n"); + for(i=0; ip==0 ){ - session_not_open: - raw_printf(stderr, "ERROR: No sessions are open\n"); + /* convert testctrl text option to value. allow any unique prefix + ** of the option name, or a numerical value. */ + n2 = strlen30(zCmd); + for(i=0; ip, azCmd[1]); - if( rc ){ - raw_printf(stderr, "ERROR: sqlite3session_attach() returns %d\n", rc); - rc = 0; - } + *pzErr = smprintf + ("Error: ambiguous test-control: \"%s\"\n" + "Use \".testctrl --help\" for help\n", zCmd); + return DCR_ArgWrong; } - }else + } + } + if( testctrl<0 ){ + utf8_printf(STD_ERR,"Error: unknown test-control: %s\n" + "Use \".testctrl --help\" for help\n", zCmd); + }else if( aCtrl[iCtrl].unSafe && ISS(p)->bSafeMode ){ + utf8_printf(STD_ERR, + "line %d: \".testctrl %s\" may not be used in safe mode\n", + ISS(p)->pInSource->lineno, aCtrl[iCtrl].zCtrlName); + exit(1); + }else{ + switch(testctrl){ - /* .session changeset FILE - ** .session patchset FILE - ** Write a changeset or patchset into a file. The file is overwritten. - */ - if( strcmp(azCmd[0],"changeset")==0 || strcmp(azCmd[0],"patchset")==0 ){ - FILE *out = 0; - failIfSafeMode(p, "cannot run \".session %s\" in safe mode", azCmd[0]); - if( nCmd!=2 ) goto session_syntax_error; - if( pSession->p==0 ) goto session_not_open; - out = fopen(azCmd[1], "wb"); - if( out==0 ){ - utf8_printf(stderr, "ERROR: cannot open \"%s\" for writing\n", - azCmd[1]); - }else{ - int szChng; - void *pChng; - if( azCmd[0][0]=='c' ){ - rc = sqlite3session_changeset(pSession->p, &szChng, &pChng); - }else{ - rc = sqlite3session_patchset(pSession->p, &szChng, &pChng); - } - if( rc ){ - printf("Error: error code %d\n", rc); - rc = 0; - } - if( pChng - && fwrite(pChng, szChng, 1, out)!=1 ){ - raw_printf(stderr, "ERROR: Failed to write entire %d-byte output\n", - szChng); - } - sqlite3_free(pChng); - fclose(out); + /* sqlite3_test_control(int, db, int) */ + case SQLITE_TESTCTRL_OPTIMIZATIONS: + if( nArg==3 ){ + unsigned int opt = (unsigned int)strtol(azArg[2], 0, 0); + rc2 = sqlite3_test_control(testctrl, DBX(p), opt); + isOk = 3; } - }else + break; - /* .session close - ** Close the identified session - */ - if( strcmp(azCmd[0], "close")==0 ){ - if( nCmd!=1 ) goto session_syntax_error; - if( pAuxDb->nSession ){ - session_close(pSession); - pAuxDb->aSession[iSes] = pAuxDb->aSession[--pAuxDb->nSession]; + /* sqlite3_test_control(int) */ + case SQLITE_TESTCTRL_PRNG_SAVE: + case SQLITE_TESTCTRL_PRNG_RESTORE: + case SQLITE_TESTCTRL_BYTEORDER: + if( nArg==2 ){ + rc2 = sqlite3_test_control(testctrl); + isOk = testctrl==SQLITE_TESTCTRL_BYTEORDER ? 1 : 3; } - }else + break; - /* .session enable ?BOOLEAN? - ** Query or set the enable flag - */ - if( strcmp(azCmd[0], "enable")==0 ){ - int ii; - if( nCmd>2 ) goto session_syntax_error; - ii = nCmd==1 ? -1 : booleanValue(azCmd[1]); - if( pAuxDb->nSession ){ - ii = sqlite3session_enable(pSession->p, ii); - utf8_printf(p->out, "session %s enable flag = %d\n", - pSession->zName, ii); + /* sqlite3_test_control(int, uint) */ + case SQLITE_TESTCTRL_PENDING_BYTE: + if( nArg==3 ){ + unsigned int opt = (unsigned int)integerValue(azArg[2]); + rc2 = sqlite3_test_control(testctrl, opt); + isOk = 3; } - }else + break; - /* .session filter GLOB .... - ** Set a list of GLOB patterns of table names to be excluded. - */ - if( strcmp(azCmd[0], "filter")==0 ){ - int ii, nByte; - if( nCmd<2 ) goto session_syntax_error; - if( pAuxDb->nSession ){ - for(ii=0; iinFilter; ii++){ - sqlite3_free(pSession->azFilter[ii]); + /* sqlite3_test_control(int, int, sqlite3*) */ + case SQLITE_TESTCTRL_PRNG_SEED: + if( nArg==3 || nArg==4 ){ + int ii = (int)integerValue(azArg[2]); + sqlite3 *db; + if( ii==0 && strcmp(azArg[2],"random")==0 ){ + sqlite3_randomness(sizeof(ii),&ii); + fprintf(STD_OUT, "-- random seed: %d\n", ii); } - sqlite3_free(pSession->azFilter); - nByte = sizeof(pSession->azFilter[0])*(nCmd-1); - pSession->azFilter = sqlite3_malloc( nByte ); - if( pSession->azFilter==0 ){ - raw_printf(stderr, "Error: out or memory\n"); - exit(1); - } - for(ii=1; iiazFilter[ii-1] = sqlite3_mprintf("%s", azCmd[ii]); - shell_check_oom(x); + if( nArg==3 ){ + db = 0; + }else{ + db = DBX(p); + /* Make sure the schema has been loaded */ + sqlite3_table_column_metadata(db, 0, "x", 0, 0, 0, 0, 0, 0); } - pSession->nFilter = ii-1; + rc2 = sqlite3_test_control(testctrl, ii, db); + isOk = 3; } - }else + break; - /* .session indirect ?BOOLEAN? - ** Query or set the indirect flag - */ - if( strcmp(azCmd[0], "indirect")==0 ){ - int ii; - if( nCmd>2 ) goto session_syntax_error; - ii = nCmd==1 ? -1 : booleanValue(azCmd[1]); - if( pAuxDb->nSession ){ - ii = sqlite3session_indirect(pSession->p, ii); - utf8_printf(p->out, "session %s indirect flag = %d\n", - pSession->zName, ii); + /* sqlite3_test_control(int, int) */ + case SQLITE_TESTCTRL_ASSERT: + case SQLITE_TESTCTRL_ALWAYS: + if( nArg==3 ){ + int opt = booleanValue(azArg[2]); + rc2 = sqlite3_test_control(testctrl, opt); + isOk = 1; } - }else + break; - /* .session isempty - ** Determine if the session is empty - */ - if( strcmp(azCmd[0], "isempty")==0 ){ - int ii; - if( nCmd!=1 ) goto session_syntax_error; - if( pAuxDb->nSession ){ - ii = sqlite3session_isempty(pSession->p); - utf8_printf(p->out, "session %s isempty flag = %d\n", - pSession->zName, ii); + /* sqlite3_test_control(int, int) */ + case SQLITE_TESTCTRL_LOCALTIME_FAULT: + case SQLITE_TESTCTRL_NEVER_CORRUPT: + if( nArg==3 ){ + int opt = booleanValue(azArg[2]); + rc2 = sqlite3_test_control(testctrl, opt); + isOk = 3; } - }else + break; - /* .session list - ** List all currently open sessions - */ - if( strcmp(azCmd[0],"list")==0 ){ - for(i=0; inSession; i++){ - utf8_printf(p->out, "%d %s\n", i, pAuxDb->aSession[i].zName); - } - }else + /* sqlite3_test_control(sqlite3*) */ + case SQLITE_TESTCTRL_INTERNAL_FUNCTIONS: + rc2 = sqlite3_test_control(testctrl, DBX(p)); + isOk = 3; + break; - /* .session open DB NAME - ** Open a new session called NAME on the attached database DB. - ** DB is normally "main". - */ - if( strcmp(azCmd[0],"open")==0 ){ - char *zName; - if( nCmd!=3 ) goto session_syntax_error; - zName = azCmd[2]; - if( zName[0]==0 ) goto session_syntax_error; - for(i=0; inSession; i++){ - if( strcmp(pAuxDb->aSession[i].zName,zName)==0 ){ - utf8_printf(stderr, "Session \"%s\" already exists\n", zName); - goto meta_command_exit; - } + case SQLITE_TESTCTRL_IMPOSTER: + if( nArg==5 ){ + rc2 = sqlite3_test_control(testctrl, DBX(p), + azArg[2], + integerValue(azArg[3]), + integerValue(azArg[4])); + isOk = 3; } - if( pAuxDb->nSession>=ArraySize(pAuxDb->aSession) ){ - raw_printf(stderr, "Maximum of %d sessions\n", ArraySize(pAuxDb->aSession)); - goto meta_command_exit; - } - pSession = &pAuxDb->aSession[pAuxDb->nSession]; - rc = sqlite3session_create(p->db, azCmd[1], &pSession->p); - if( rc ){ - raw_printf(stderr, "Cannot open session: error code=%d\n", rc); - rc = 0; - goto meta_command_exit; + break; + + case SQLITE_TESTCTRL_SEEK_COUNT: { + u64 x = 0; + rc2 = sqlite3_test_control(testctrl, DBX(p), &x); + utf8_printf(out, "%llu\n", x); + isOk = 3; + break; + } + +#ifdef YYCOVERAGE + case SQLITE_TESTCTRL_PARSER_COVERAGE: { + if( nArg==2 ){ + sqlite3_test_control(testctrl, out); + isOk = 3; } - pSession->nFilter = 0; - sqlite3session_table_filter(pSession->p, session_filter, pSession); - pAuxDb->nSession++; - pSession->zName = sqlite3_mprintf("%s", zName); - shell_check_oom(pSession->zName); - }else - /* If no command name matches, show a syntax error */ - session_syntax_error: - showHelp(p->out, "session"); - }else + break; + } #endif - #ifdef SQLITE_DEBUG - /* Undocumented commands for internal testing. Subject to change - ** without notice. */ - if( c=='s' && n>=10 && strncmp(azArg[0], "selftest-", 9)==0 ){ - if( strncmp(azArg[0]+9, "boolean", n-9)==0 ){ - int i, v; - for(i=1; iout, "%s: %d 0x%x\n", azArg[i], v, v); + case SQLITE_TESTCTRL_TUNE: { + if( nArg==4 ){ + int id = (int)integerValue(azArg[2]); + int val = (int)integerValue(azArg[3]); + sqlite3_test_control(testctrl, id, &val); + isOk = 3; + }else if( nArg==3 ){ + int id = (int)integerValue(azArg[2]); + sqlite3_test_control(testctrl, -id, &rc2); + isOk = 1; + }else if( nArg==2 ){ + int id = 1; + while(1){ + int val = 0; + rc2 = sqlite3_test_control(testctrl, -id, &val); + if( rc2!=SQLITE_OK ) break; + if( id>1 ) utf8_printf(out, " "); + utf8_printf(out, "%d: %d", id, val); + id++; + } + if( id>1 ) utf8_printf(out, "\n"); + isOk = 3; } + break; } - if( strncmp(azArg[0]+9, "integer", n-9)==0 ){ - int i; sqlite3_int64 v; - for(i=1; iout, "%s", zBuf); - } +#endif } - }else + } + if( isOk==0 && iCtrl>=0 ){ + utf8_printf(out, "Usage: .testctrl %s %s\n", zCmd,aCtrl[iCtrl].zUsage); + return DCR_CmdErred; + }else if( isOk==1 ){ + raw_printf(out, "%d\n", rc2); + }else if( isOk==2 ){ + raw_printf(out, "0x%08x\n", rc2); + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( timeout 4 1 2 ){ + open_db(p, 0); + sqlite3_busy_timeout(DBX(p), nArg>=2 ? (int)integerValue(azArg[1]) : 0); + return DCR_Ok; +} +DISPATCHABLE_COMMAND( timer ? 2 2 ){ + enableTimer = booleanValue(azArg[1]); + if( enableTimer && !HAS_TIMER ){ + raw_printf(STD_ERR, "Error: timer not available on this system.\n"); + enableTimer = 0; + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( trace ? 0 0 ){ + ShellInState *psi = ISS(p); + int mType = 0; + int jj; + open_db(p, 0); + for(jj=1; jjeTraceType = SHELL_TRACE_EXPANDED; + } +#ifdef SQLITE_ENABLE_NORMALIZE + else if( optionMatch(z, "normalized") ){ + psi->eTraceType = SHELL_TRACE_NORMALIZED; + } #endif - - if( c=='s' && n>=4 && strncmp(azArg[0],"selftest",n)==0 ){ - int bIsInit = 0; /* True to initialize the SELFTEST table */ - int bVerbose = 0; /* Verbose output */ - int bSelftestExists; /* True if SELFTEST already exists */ - int i, k; /* Loop counters */ - int nTest = 0; /* Number of tests runs */ - int nErr = 0; /* Number of errors seen */ - ShellText str; /* Answer for a query */ - sqlite3_stmt *pStmt = 0; /* Query against the SELFTEST table */ - - open_db(p,0); - for(i=1; ieTraceType = SHELL_TRACE_PLAIN; } - } - if( sqlite3_table_column_metadata(p->db,"main","selftest",0,0,0,0,0,0) - != SQLITE_OK ){ - bSelftestExists = 0; - }else{ - bSelftestExists = 1; - } - if( bIsInit ){ - createSelftestTable(p); - bSelftestExists = 1; - } - initText(&str); - appendText(&str, "x", 0); - for(k=bSelftestExists; k>=0; k--){ - if( k==1 ){ - rc = sqlite3_prepare_v2(p->db, - "SELECT tno,op,cmd,ans FROM selftest ORDER BY tno", - -1, &pStmt, 0); - }else{ - rc = sqlite3_prepare_v2(p->db, - "VALUES(0,'memo','Missing SELFTEST table - default checks only','')," - " (1,'run','PRAGMA integrity_check','ok')", - -1, &pStmt, 0); + else if( optionMatch(z, "profile") ){ + mType |= SQLITE_TRACE_PROFILE; } - if( rc ){ - raw_printf(stderr, "Error querying the selftest table\n"); - rc = 1; - sqlite3_finalize(pStmt); - goto meta_command_exit; - } - for(i=1; sqlite3_step(pStmt)==SQLITE_ROW; i++){ - int tno = sqlite3_column_int(pStmt, 0); - const char *zOp = (const char*)sqlite3_column_text(pStmt, 1); - const char *zSql = (const char*)sqlite3_column_text(pStmt, 2); - const char *zAns = (const char*)sqlite3_column_text(pStmt, 3); - - if( zOp==0 ) continue; - if( zSql==0 ) continue; - if( zAns==0 ) continue; - k = 0; - if( bVerbose>0 ){ - printf("%d: %s %s\n", tno, zOp, zSql); - } - if( strcmp(zOp,"memo")==0 ){ - utf8_printf(p->out, "%s\n", zSql); - }else - if( strcmp(zOp,"run")==0 ){ - char *zErrMsg = 0; - str.n = 0; - str.z[0] = 0; - rc = sqlite3_exec(p->db, zSql, captureOutputCallback, &str, &zErrMsg); - nTest++; - if( bVerbose ){ - utf8_printf(p->out, "Result: %s\n", str.z); - } - if( rc || zErrMsg ){ - nErr++; - rc = 1; - utf8_printf(p->out, "%d: error-code-%d: %s\n", tno, rc, zErrMsg); - sqlite3_free(zErrMsg); - }else if( strcmp(zAns,str.z)!=0 ){ - nErr++; - rc = 1; - utf8_printf(p->out, "%d: Expected: [%s]\n", tno, zAns); - utf8_printf(p->out, "%d: Got: [%s]\n", tno, str.z); - } - }else - { - utf8_printf(stderr, - "Unknown operation \"%s\" on selftest line %d\n", zOp, tno); - rc = 1; - break; - } - } /* End loop over rows of content from SELFTEST */ - sqlite3_finalize(pStmt); - } /* End loop over k */ - freeText(&str); - utf8_printf(p->out, "%d errors out of %d tests\n", nErr, nTest); - }else - - if( c=='s' && strncmp(azArg[0], "separator", n)==0 ){ - if( nArg<2 || nArg>3 ){ - raw_printf(stderr, "Usage: .separator COL ?ROW?\n"); - rc = 1; - } - if( nArg>=2 ){ - sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, - "%.*s", (int)ArraySize(p->colSeparator)-1, azArg[1]); - } - if( nArg>=3 ){ - sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, - "%.*s", (int)ArraySize(p->rowSeparator)-1, azArg[2]); - } - }else - - if( c=='s' && n>=4 && strncmp(azArg[0],"sha3sum",n)==0 ){ - const char *zLike = 0; /* Which table to checksum. 0 means everything */ - int i; /* Loop counter */ - int bSchema = 0; /* Also hash the schema */ - int bSeparate = 0; /* Hash each table separately */ - int iSize = 224; /* Hash algorithm to use */ - int bDebug = 0; /* Only show the query that would have run */ - sqlite3_stmt *pStmt; /* For querying tables names */ - char *zSql; /* SQL to be run */ - char *zSep; /* Separator */ - ShellText sSql; /* Complete SQL for the query to run the hash */ - ShellText sQuery; /* Set of queries used to read all content */ - open_db(p, 0); - for(i=1; iout, azArg[0]); - rc = 1; - goto meta_command_exit; - } - }else if( zLike ){ - raw_printf(stderr, "Usage: .sha3sum ?OPTIONS? ?LIKE-PATTERN?\n"); - rc = 1; - goto meta_command_exit; - }else{ - zLike = z; - bSeparate = 1; - if( sqlite3_strlike("sqlite\\_%", zLike, '\\')==0 ) bSchema = 1; + else if( optionMatch(z, "row") ){ + mType |= SQLITE_TRACE_ROW; + } + else if( optionMatch(z, "stmt") ){ + mType |= SQLITE_TRACE_STMT; + } + else if( optionMatch(z, "close") ){ + mType |= SQLITE_TRACE_CLOSE; + } + else { + return DCR_Unknown|jj; } - } - if( bSchema ){ - zSql = "SELECT lower(name) FROM sqlite_schema" - " WHERE type='table' AND coalesce(rootpage,0)>1" - " UNION ALL SELECT 'sqlite_schema'" - " ORDER BY 1 collate nocase"; - }else{ - zSql = "SELECT lower(name) FROM sqlite_schema" - " WHERE type='table' AND coalesce(rootpage,0)>1" - " AND name NOT LIKE 'sqlite_%'" - " ORDER BY 1 collate nocase"; - } - sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); - initText(&sQuery); - initText(&sSql); - appendText(&sSql, "WITH [sha3sum$query](a,b) AS(",0); - zSep = "VALUES("; - while( SQLITE_ROW==sqlite3_step(pStmt) ){ - const char *zTab = (const char*)sqlite3_column_text(pStmt,0); - if( zTab==0 ) continue; - if( zLike && sqlite3_strlike(zLike, zTab, 0)!=0 ) continue; - if( strncmp(zTab, "sqlite_",7)!=0 ){ - appendText(&sQuery,"SELECT * FROM ", 0); - appendText(&sQuery,zTab,'"'); - appendText(&sQuery," NOT INDEXED;", 0); - }else if( strcmp(zTab, "sqlite_schema")==0 ){ - appendText(&sQuery,"SELECT type,name,tbl_name,sql FROM sqlite_schema" - " ORDER BY name;", 0); - }else if( strcmp(zTab, "sqlite_sequence")==0 ){ - appendText(&sQuery,"SELECT name,seq FROM sqlite_sequence" - " ORDER BY name;", 0); - }else if( strcmp(zTab, "sqlite_stat1")==0 ){ - appendText(&sQuery,"SELECT tbl,idx,stat FROM sqlite_stat1" - " ORDER BY tbl,idx;", 0); - }else if( strcmp(zTab, "sqlite_stat4")==0 ){ - appendText(&sQuery, "SELECT * FROM ", 0); - appendText(&sQuery, zTab, 0); - appendText(&sQuery, " ORDER BY tbl, idx, rowid;\n", 0); - } - appendText(&sSql, zSep, 0); - appendText(&sSql, sQuery.z, '\''); - sQuery.n = 0; - appendText(&sSql, ",", 0); - appendText(&sSql, zTab, '\''); - zSep = "),("; - } - sqlite3_finalize(pStmt); - if( bSeparate ){ - zSql = sqlite3_mprintf( - "%s))" - " SELECT lower(hex(sha3_query(a,%d))) AS hash, b AS label" - " FROM [sha3sum$query]", - sSql.z, iSize); - }else{ - zSql = sqlite3_mprintf( - "%s))" - " SELECT lower(hex(sha3_query(group_concat(a,''),%d))) AS hash" - " FROM [sha3sum$query]", - sSql.z, iSize); - } - shell_check_oom(zSql); - freeText(&sQuery); - freeText(&sSql); - if( bDebug ){ - utf8_printf(p->out, "%s\n", zSql); }else{ - shell_exec(p, zSql, 0); + output_file_close(psi->traceOut); + psi->traceOut = output_file_open(azArg[1], 0); } - sqlite3_free(zSql); - }else + } + if( psi->traceOut==0 ){ + sqlite3_trace_v2(DBX(p), 0, 0, 0); + }else{ + if( mType==0 ) mType = SQLITE_TRACE_STMT; + sqlite3_trace_v2(DBX(p), mType, sql_trace_callback, psi); + } + return DCR_Ok; +} -#ifndef SQLITE_NOHAVE_SYSTEM - if( c=='s' - && (strncmp(azArg[0], "shell", n)==0 || strncmp(azArg[0],"system",n)==0) - ){ - char *zCmd; - int i, x; - failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]); - if( nArg<2 ){ - raw_printf(stderr, "Usage: .system COMMAND\n"); - rc = 1; - goto meta_command_exit; - } - zCmd = sqlite3_mprintf(strchr(azArg[1],' ')==0?"%s":"\"%s\"", azArg[1]); - for(i=2; i=3 && strncmp(zOpt, "-allexcept",lenOpt)==0 ){ + assert( azArg[nArg]==0 ); + sqlite3_drop_modules(DBX(p), nArg>2 ? (const char**)(azArg+2) : 0); + }else{ + for(ii=1; iiout, "%12.12s: %s\n","echo", - azBool[ShellHasFlag(p, SHFLG_Echo)]); - utf8_printf(p->out, "%12.12s: %s\n","eqp", azBool[p->autoEQP&3]); - utf8_printf(p->out, "%12.12s: %s\n","explain", - p->mode==MODE_Explain ? "on" : p->autoExplain ? "auto" : "off"); - utf8_printf(p->out,"%12.12s: %s\n","headers", azBool[p->showHeader!=0]); - if( p->mode==MODE_Column - || (p->mode>=MODE_Markdown && p->mode<=MODE_Box) - ){ - utf8_printf - (p->out, "%12.12s: %s --wrap %d --wordwrap %s --%squote\n", "mode", - modeDescr[p->mode], p->cmOpts.iWrap, - p->cmOpts.bWordWrap ? "on" : "off", - p->cmOpts.bQuote ? "" : "no"); - }else{ - utf8_printf(p->out, "%12.12s: %s\n","mode", modeDescr[p->mode]); - } - utf8_printf(p->out, "%12.12s: ", "nullvalue"); - output_c_string(p->out, p->nullValue); - raw_printf(p->out, "\n"); - utf8_printf(p->out,"%12.12s: %s\n","output", - strlen30(p->outfile) ? p->outfile : "stdout"); - utf8_printf(p->out,"%12.12s: ", "colseparator"); - output_c_string(p->out, p->colSeparator); - raw_printf(p->out, "\n"); - utf8_printf(p->out,"%12.12s: ", "rowseparator"); - output_c_string(p->out, p->rowSeparator); - raw_printf(p->out, "\n"); - switch( p->statsOn ){ - case 0: zOut = "off"; break; - default: zOut = "on"; break; - case 2: zOut = "stmt"; break; - case 3: zOut = "vmstep"; break; - } - utf8_printf(p->out, "%12.12s: %s\n","stats", zOut); - utf8_printf(p->out, "%12.12s: ", "width"); - for (i=0;inWidth;i++) { - raw_printf(p->out, "%d ", p->colWidth[i]); - } - raw_printf(p->out, "\n"); - utf8_printf(p->out, "%12.12s: %s\n", "filename", - p->pAuxDb->zDbFilename ? p->pAuxDb->zDbFilename : ""); - }else + } + return DCR_Ok; +} - if( c=='s' && strncmp(azArg[0], "stats", n)==0 ){ - if( nArg==2 ){ - if( strcmp(azArg[1],"stmt")==0 ){ - p->statsOn = 2; - }else if( strcmp(azArg[1],"vmstep")==0 ){ - p->statsOn = 3; - }else{ - p->statsOn = (u8)booleanValue(azArg[1]); - } - }else if( nArg==1 ){ - display_stats(p->db, p, 0); - }else{ - raw_printf(stderr, "Usage: .stats ?on|off|stmt|vmstep?\n"); - rc = 1; +/***************** + * The .user command + * Because there is no help text for .user, it does its own argument validation. + */ +CONDITION_COMMAND( user SQLITE_USER_AUTHENTICATION ); +DISPATCHABLE_COMMAND( user ? 0 0 ){ + int rc; + const char *usage + = "Usage: .user SUBCOMMAND ...\n" + "Subcommands are:\n" + " login USER PASSWORD\n" + " delete USER\n" + " add USER PASSWORD ISADMIN\n" + " edit USER PASSWORD ISADMIN\n" + ; + if( nArg<2 ){ + teach_fail: + *pzErr = smprintf(usage); + return DCR_SayUsage; + } + open_db(p, 0); + if( strcmp(azArg[1],"login")==0 ){ + if( nArg!=4 ){ + goto teach_fail; } - }else - - if( (c=='t' && n>1 && strncmp(azArg[0], "tables", n)==0) - || (c=='i' && (strncmp(azArg[0], "indices", n)==0 - || strncmp(azArg[0], "indexes", n)==0) ) - ){ - sqlite3_stmt *pStmt; - char **azResult; - int nRow, nAlloc; - int ii; - ShellText s; - initText(&s); - open_db(p, 0); - rc = sqlite3_prepare_v2(p->db, "PRAGMA database_list", -1, &pStmt, 0); + rc = sqlite3_user_authenticate(DBX(p), azArg[2], azArg[3], + strlen30(azArg[3])); if( rc ){ - sqlite3_finalize(pStmt); - return shellDatabaseError(p->db); + *pzErr = shellMPrintf(0,"Authentication failed for user %s\n", azArg[2]); + return DCR_Error; } - - if( nArg>2 && c=='i' ){ - /* It is an historical accident that the .indexes command shows an error - ** when called with the wrong number of arguments whereas the .tables - ** command does not. */ - raw_printf(stderr, "Usage: .indexes ?LIKE-PATTERN?\n"); - rc = 1; - sqlite3_finalize(pStmt); - goto meta_command_exit; - } - for(ii=0; sqlite3_step(pStmt)==SQLITE_ROW; ii++){ - const char *zDbName = (const char*)sqlite3_column_text(pStmt, 1); - if( zDbName==0 ) continue; - if( s.z && s.z[0] ) appendText(&s, " UNION ALL ", 0); - if( sqlite3_stricmp(zDbName, "main")==0 ){ - appendText(&s, "SELECT name FROM ", 0); - }else{ - appendText(&s, "SELECT ", 0); - appendText(&s, zDbName, '\''); - appendText(&s, "||'.'||name FROM ", 0); - } - appendText(&s, zDbName, '"'); - appendText(&s, ".sqlite_schema ", 0); - if( c=='t' ){ - appendText(&s," WHERE type IN ('table','view')" - " AND name NOT LIKE 'sqlite_%'" - " AND name LIKE ?1", 0); - }else{ - appendText(&s," WHERE type='index'" - " AND tbl_name LIKE ?1", 0); - } + }else if( strcmp(azArg[1],"add")==0 ){ + if( nArg!=5 ){ + goto teach_fail; } - rc = sqlite3_finalize(pStmt); - if( rc==SQLITE_OK ){ - appendText(&s, " ORDER BY 1", 0); - rc = sqlite3_prepare_v2(p->db, s.z, -1, &pStmt, 0); + rc = sqlite3_user_add(DBX(p), azArg[2], azArg[3], strlen30(azArg[3]), + booleanValue(azArg[4])); + if( rc ){ + *pzErr = shellMPrintf(0,"User-Add failed: %d\n", rc); + return DCR_Error; } - freeText(&s); - if( rc ) return shellDatabaseError(p->db); - - /* Run the SQL statement prepared by the above block. Store the results - ** as an array of nul-terminated strings in azResult[]. */ - nRow = nAlloc = 0; - azResult = 0; - if( nArg>1 ){ - sqlite3_bind_text(pStmt, 1, azArg[1], -1, SQLITE_TRANSIENT); - }else{ - sqlite3_bind_text(pStmt, 1, "%", -1, SQLITE_STATIC); + }else if( strcmp(azArg[1],"edit")==0 ){ + if( nArg!=5 ){ + goto teach_fail; } - while( sqlite3_step(pStmt)==SQLITE_ROW ){ - if( nRow>=nAlloc ){ - char **azNew; - int n2 = nAlloc*2 + 10; - azNew = sqlite3_realloc64(azResult, sizeof(azResult[0])*n2); - shell_check_oom(azNew); - nAlloc = n2; - azResult = azNew; - } - azResult[nRow] = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 0)); - shell_check_oom(azResult[nRow]); - nRow++; - } - if( sqlite3_finalize(pStmt)!=SQLITE_OK ){ - rc = shellDatabaseError(p->db); - } - - /* Pretty-print the contents of array azResult[] to the output */ - if( rc==0 && nRow>0 ){ - int len, maxlen = 0; - int i, j; - int nPrintCol, nPrintRow; - for(i=0; imaxlen ) maxlen = len; - } - nPrintCol = 80/(maxlen+2); - if( nPrintCol<1 ) nPrintCol = 1; - nPrintRow = (nRow + nPrintCol - 1)/nPrintCol; - for(i=0; iout, "%s%-*s", zSp, maxlen, - azResult[j] ? azResult[j]:""); - } - raw_printf(p->out, "\n"); - } + rc = sqlite3_user_change(DBX(p), azArg[2], azArg[3], strlen30(azArg[3]), + booleanValue(azArg[4])); + if( rc ){ + *pzErr = shellMPrintf(0,"User-Edit failed: %d\n", rc); + return DCR_Error; } - - for(ii=0; iiout = output_file_open("testcase-out.txt", 0); - if( p->out==0 ){ - raw_printf(stderr, "Error: cannot open 'testcase-out.txt'\n"); + }else if( strcmp(azArg[1],"delete")==0 ){ + if( nArg!=3 ){ + goto teach_fail; } - if( nArg>=2 ){ - sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "%s", azArg[1]); - }else{ - sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "?"); + rc = sqlite3_user_delete(DBX(p), azArg[2]); + if( rc ){ + *pzErr = shellMPrintf(0,"User-Delete failed: %d\n", rc); + return DCR_Error; } - }else + }else{ + goto teach_fail; + } + return DCR_Ok; +} -#ifndef SQLITE_UNTESTABLE - if( c=='t' && n>=8 && strncmp(azArg[0], "testctrl", n)==0 ){ - static const struct { - const char *zCtrlName; /* Name of a test-control option */ - int ctrlCode; /* Integer code for that option */ - int unSafe; /* Not valid for --safe mode */ - const char *zUsage; /* Usage notes */ - } aCtrl[] = { - { "always", SQLITE_TESTCTRL_ALWAYS, 1, "BOOLEAN" }, - { "assert", SQLITE_TESTCTRL_ASSERT, 1, "BOOLEAN" }, - /*{ "benign_malloc_hooks",SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS,1, "" },*/ - /*{ "bitvec_test", SQLITE_TESTCTRL_BITVEC_TEST, 1, "" },*/ - { "byteorder", SQLITE_TESTCTRL_BYTEORDER, 0, "" }, - { "extra_schema_checks",SQLITE_TESTCTRL_EXTRA_SCHEMA_CHECKS,0,"BOOLEAN" }, - /*{ "fault_install", SQLITE_TESTCTRL_FAULT_INSTALL, 1,"" },*/ - { "imposter", SQLITE_TESTCTRL_IMPOSTER,1,"SCHEMA ON/OFF ROOTPAGE"}, - { "internal_functions", SQLITE_TESTCTRL_INTERNAL_FUNCTIONS,0,"" }, - { "localtime_fault", SQLITE_TESTCTRL_LOCALTIME_FAULT,0,"BOOLEAN" }, - { "never_corrupt", SQLITE_TESTCTRL_NEVER_CORRUPT,1, "BOOLEAN" }, - { "optimizations", SQLITE_TESTCTRL_OPTIMIZATIONS,0,"DISABLE-MASK" }, -#ifdef YYCOVERAGE - { "parser_coverage", SQLITE_TESTCTRL_PARSER_COVERAGE,0,"" }, -#endif - { "pending_byte", SQLITE_TESTCTRL_PENDING_BYTE,0, "OFFSET " }, - { "prng_restore", SQLITE_TESTCTRL_PRNG_RESTORE,0, "" }, - { "prng_save", SQLITE_TESTCTRL_PRNG_SAVE, 0, "" }, - { "prng_seed", SQLITE_TESTCTRL_PRNG_SEED, 0, "SEED ?db?" }, - { "seek_count", SQLITE_TESTCTRL_SEEK_COUNT, 0, "" }, - { "sorter_mmap", SQLITE_TESTCTRL_SORTER_MMAP, 0, "NMAX" }, - { "tune", SQLITE_TESTCTRL_TUNE, 1, "ID VALUE" }, - }; - int testctrl = -1; - int iCtrl = -1; - int rc2 = 0; /* 0: usage. 1: %d 2: %x 3: no-output */ - int isOk = 0; - int i, n2; - const char *zCmd = 0; +/***************** + * The .vars command + */ +COLLECT_HELP_TEXT[ + ".vars ?OPTIONS? ... Manipulate and display shell variables", + " clear ?NAMES? Erase all or only given named variables", +#ifndef SQLITE_NOHAVE_SYSTEM + " edit ?-e? NAME Use edit() to create or alter variable NAME", + " With a -e option, the edited value is evaluated as a SQL expression.", +#endif + " list ?PATTERNS? List shell variables table values", + " Alternatively, to list just some or all names: ls ?PATTERNS?", + " load ?FILE? ?NAMES? Load some or all named variables from FILE", + " If FILE missing, empty or '~', it defaults to ~/sqlite_vars.sdb", + " save ?FILE? ?NAMES? Save some or all named variables into FILE", + " If FILE missing, empty or '~', it defaults to ~/sqlite_vars.sdb", + " set NAME VALUE Give shell variable NAME a value of VALUE", + " NAME must begin with a letter to be executable by .x, Other leading", + " characters have special uses. VALUE is the space-joined arguments.", + " unset ?NAMES? Remove named variables(s) from variables table", +]; +DISPATCHABLE_COMMAND( vars 2 1 0 ){ + DotCmdRC rv = DCR_Ok; + sqlite3 *dbs = p->dbShell; + const char *zCmd = (nArg>1)? azArg[1] : "ls"; + int rc = 0; + int ncCmd = strlen30(zCmd); - open_db(p, 0); - zCmd = nArg>=2 ? azArg[1] : "help"; + if( *zCmd>'e' && ncCmd<2 ) return DCR_Ambiguous|1; +#define SUBCMD(scn) (strncmp(zCmd, scn, ncCmd)==0) - /* The argument can optionally begin with "-" or "--" */ - if( zCmd[0]=='-' && zCmd[1] ){ - zCmd++; - if( zCmd[0]=='-' && zCmd[1] ) zCmd++; - } + /* This could be done lazily, but with more code. */ + if( (dbs && ensure_shvars_table(dbs)!=SQLITE_OK) ){ + return DCR_Error; + }else{ + if( ensure_shell_db(p)!=SQLITE_OK ) return DCR_Error; + dbs = p->dbShell; + assert(dbs!=0); + if( ensure_shvars_table(dbs)!=SQLITE_OK ) return DCR_Error; + } - /* --help lists all test-controls */ - if( strcmp(zCmd,"help")==0 ){ - utf8_printf(p->out, "Available test-controls:\n"); - for(i=0; iout, " .testctrl %s %s\n", - aCtrl[i].zCtrlName, aCtrl[i].zUsage); - } - rc = 1; - goto meta_command_exit; + /* .vars clear and .vars unset ?NAMES? + ** Delete some or all key/value pairs from the shell variables table. + ** Without any arguments, clear deletes them all and unset does nothing. + */ + if( SUBCMD("clear") || SUBCMD("unset") ){ + if( (nArg>2 || zCmd[0]=='c') ){ + sqlite3_str *sbZap = sqlite3_str_new(dbs); + char *zSql; + sqlite3_str_appendf + (sbZap, "DELETE FROM "SHVAR_TABLE_SNAME" WHERE key "); + append_in_clause(sbZap, + (const char **)&azArg[2], (const char **)&azArg[nArg]); + zSql = sqlite3_str_finish(sbZap); + shell_check_oom(zSql); + rc = sqlite3_exec(dbs, zSql, 0, 0, 0); + sqlite3_free(zSql); } - - /* convert testctrl text option to value. allow any unique prefix - ** of the option name, or a numerical value. */ - n2 = strlen30(zCmd); - for(i=0; ipInSource) ){ + utf8_printf(STD_ERR, "Error: " + ".vars edit can only be used interactively.\n"); + return DCR_Error; + } + edSet = attempt_editor_set(psi, azArg[0], (nArg>2)? azArg[2] : 0 ); + if( edSet < 0 ) return DCR_Error; + else ia += edSet; + while( ia < nArg ){ + ParamTableUse ptu; + char *zA = azArg[ia]; + char cf = (zA[0]=='-')? zA[1] : 0; + if( cf!=0 && zA[2]==0 ){ + ++ia; + switch( cf ){ + case 'e': eval = 1; continue; + case 't': eval = 0; continue; + default: + utf8_printf(STD_ERR, "Error: bad .vars edit option: %s\n", zA); + return DCR_Error; } } - } - if( testctrl<0 ){ - utf8_printf(stderr,"Error: unknown test-control: %s\n" - "Use \".testctrl --help\" for help\n", zCmd); - }else if( aCtrl[iCtrl].unSafe && p->bSafeMode ){ - utf8_printf(stderr, - "line %d: \".testctrl %s\" may not be used in safe mode\n", - p->lineno, aCtrl[iCtrl].zCtrlName); - exit(1); + ptu = classify_param_name(zA); + if( ptu!=PTU_Script ){ + utf8_printf(STD_ERR, + "Error: %s cannot be a shell variable name.\n", zA); + return DCR_Error; + } + rc = edit_one_kvalue(dbs, zA, eval, ptu, psi->zEditor); + ++ia; + if( rc!=0 ) return DCR_Error; + } +#endif + }else if( SUBCMD("list") || SUBCMD("ls") ){ + int nTailArgs = nArg - 1 - (nArg>1); + char **pzTailArgs = azArg + 1 + (nArg>1); + list_pov_entries(p, PTU_Script, zCmd[1]=='s', pzTailArgs, nTailArgs); + }else if( SUBCMD("load") ){ + rc = kv_pairs_load(dbs, PTU_Script, (const char **)azArg+1, nArg-1); + }else if( SUBCMD("save") ){ + rc = kv_pairs_save(dbs, PTU_Script, (const char **)azArg+1, nArg-1); + }else if( SUBCMD("set") ){ + ParamTableUse ptu; + if( nArg<4 ) return DCR_Missing; + ptu = classify_param_name(azArg[2]); + if( ptu!=PTU_Script ){ + utf8_printf(STD_ERR, + "Error: %s is not a valid shell variable name.\n", azArg[2]); + rc = 1; }else{ - switch(testctrl){ - - /* sqlite3_test_control(int, db, int) */ - case SQLITE_TESTCTRL_OPTIMIZATIONS: - if( nArg==3 ){ - unsigned int opt = (unsigned int)strtol(azArg[2], 0, 0); - rc2 = sqlite3_test_control(testctrl, p->db, opt); - isOk = 3; - } - break; - - /* sqlite3_test_control(int) */ - case SQLITE_TESTCTRL_PRNG_SAVE: - case SQLITE_TESTCTRL_PRNG_RESTORE: - case SQLITE_TESTCTRL_BYTEORDER: - if( nArg==2 ){ - rc2 = sqlite3_test_control(testctrl); - isOk = testctrl==SQLITE_TESTCTRL_BYTEORDER ? 1 : 3; - } - break; + rc = shvar_set(dbs, azArg[2], &azArg[3], &azArg[nArg]); + if( rc!=SQLITE_OK ){ + utf8_printf(STD_ERR, "Error: %s\n", sqlite3_errmsg(dbs)); + rc = 1; + } + } + }else{ + showHelp(ISS(p)->out, "vars", p); + return DCR_CmdErred; + } + return DCR_Ok | (rv!=0) | (rc!=0); +#undef SUBCMD +} - /* sqlite3_test_control(int, uint) */ - case SQLITE_TESTCTRL_PENDING_BYTE: - if( nArg==3 ){ - unsigned int opt = (unsigned int)integerValue(azArg[2]); - rc2 = sqlite3_test_control(testctrl, opt); - isOk = 3; - } - break; +/***************** + * The .vfsinfo, .vfslist, .vfsname and .version commands + */ +COLLECT_HELP_TEXT[ + ".version Show a variety of version info", + ".vfsinfo ?AUX? Information about the top-level VFS", + ".vfslist List all available VFSes", + ".vfsname ?AUX? Print the name of the VFS stack", +]; +DISPATCHABLE_COMMAND( version ? 1 1 ){ + FILE *out = ISS(p)->out; + utf8_printf(out, "SQLite %s %s\n" /*extra-version-info*/, + sqlite3_libversion(), sqlite3_sourceid()); +#if SQLITE_HAVE_ZLIB + utf8_printf(out, "zlib version %s\n", zlibVersion()); +#endif +#define CTIMEOPT_VAL_(opt) #opt +#define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt) +#if defined(__clang__) && defined(__clang_major__) + utf8_printf(out, "clang-" CTIMEOPT_VAL(__clang_major__) "." + CTIMEOPT_VAL(__clang_minor__) "." + CTIMEOPT_VAL(__clang_patchlevel__) "\n"); +#elif defined(_MSC_VER) + utf8_printf(out, "msvc-" CTIMEOPT_VAL(_MSC_VER) "\n"); +#elif defined(__GNUC__) && defined(__VERSION__) + utf8_printf(out, "gcc-" __VERSION__ "\n"); +#endif + return DCR_Ok; +} +DISPATCHABLE_COMMAND( vfsinfo ? 1 2 ){ + const char *zDbName = nArg==2 ? azArg[1] : "main"; + sqlite3_vfs *pVfs = 0; + if( DBX(p) ){ + sqlite3_file_control(DBX(p), zDbName, SQLITE_FCNTL_VFS_POINTER, &pVfs); + if( pVfs ){ + FILE *out = ISS(p)->out; + utf8_printf(out, "vfs.zName = \"%s\"\n", pVfs->zName); + raw_printf(out, "vfs.iVersion = %d\n", pVfs->iVersion); + raw_printf(out, "vfs.szOsFile = %d\n", pVfs->szOsFile); + raw_printf(out, "vfs.mxPathname = %d\n", pVfs->mxPathname); + } + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( vfslist ? 1 1 ){ + sqlite3_vfs *pVfs; + sqlite3_vfs *pCurrent = 0; + FILE *out = ISS(p)->out; + if( DBX(p) ){ + sqlite3_file_control(DBX(p), "main", SQLITE_FCNTL_VFS_POINTER, &pCurrent); + } + for(pVfs=sqlite3_vfs_find(0); pVfs; pVfs=pVfs->pNext){ + utf8_printf(out, "vfs.zName = \"%s\"%s\n", pVfs->zName, + pVfs==pCurrent ? " <--- CURRENT" : ""); + raw_printf(out, "vfs.iVersion = %d\n", pVfs->iVersion); + raw_printf(out, "vfs.szOsFile = %d\n", pVfs->szOsFile); + raw_printf(out, "vfs.mxPathname = %d\n", pVfs->mxPathname); + if( pVfs->pNext ){ + raw_printf(out, "-----------------------------------\n"); + } + } + return DCR_Ok; +} +DISPATCHABLE_COMMAND( vfsname ? 0 0 ){ + const char *zDbName = nArg==2 ? azArg[1] : "main"; + char *zVfsName = 0; + if( DBX(p) ){ + sqlite3_file_control(DBX(p), zDbName, SQLITE_FCNTL_VFSNAME, &zVfsName); + if( zVfsName ){ + utf8_printf(ISS(p)->out, "%s\n", zVfsName); + sqlite3_free(zVfsName); + } + } + return DCR_Ok; +} - /* sqlite3_test_control(int, int, sqlite3*) */ - case SQLITE_TESTCTRL_PRNG_SEED: - if( nArg==3 || nArg==4 ){ - int ii = (int)integerValue(azArg[2]); - sqlite3 *db; - if( ii==0 && strcmp(azArg[2],"random")==0 ){ - sqlite3_randomness(sizeof(ii),&ii); - printf("-- random seed: %d\n", ii); - } - if( nArg==3 ){ - db = 0; - }else{ - db = p->db; - /* Make sure the schema has been loaded */ - sqlite3_table_column_metadata(db, 0, "x", 0, 0, 0, 0, 0, 0); - } - rc2 = sqlite3_test_control(testctrl, ii, db); - isOk = 3; - } - break; +/***************** + * The .width and .wheretrace commands + * The .wheretrace command has no help. + */ +static void setColumnWidths(ShellExState *p, char *azWidths[], int nWidths){ + int j; + p->numWidths = nWidths; + p->pSpecWidths = realloc(p->pSpecWidths, (nWidths+1)*sizeof(int)*2); + if( nWidths>0 ){ + shell_check_oom(p->pSpecWidths); + p->pHaveWidths = &p->pSpecWidths[nWidths]; + for(j=0; jpSpecWidths[j] = (int)integerValue(azWidths[j]); + p->pHaveWidths[j] = 0; + } + }else p->pHaveWidths = p->pSpecWidths; +} +COLLECT_HELP_TEXT[ + ".width NUM1 NUM2 ... Set minimum column widths for columnar output", + " Negative values right-justify", +]; +DISPATCHABLE_COMMAND( width ? 1 0 ){ + setColumnWidths(p, azArg+1, nArg-1); + return DCR_Ok; +} +DISPATCHABLE_COMMAND( wheretrace ? 1 2 ){ + unsigned int x = nArg>=2 ? (unsigned int)integerValue(azArg[1]) : 0xffffffff; + sqlite3_test_control(SQLITE_TESTCTRL_TRACEFLAGS, 3, &x); + return DCR_Ok; +} - /* sqlite3_test_control(int, int) */ - case SQLITE_TESTCTRL_ASSERT: - case SQLITE_TESTCTRL_ALWAYS: - if( nArg==3 ){ - int opt = booleanValue(azArg[2]); - rc2 = sqlite3_test_control(testctrl, opt); - isOk = 1; - } - break; +/***************** + * The .x, .read and .eval commands + * These are together because they share some function and implementation. + */ - /* sqlite3_test_control(int, int) */ - case SQLITE_TESTCTRL_LOCALTIME_FAULT: - case SQLITE_TESTCTRL_NEVER_CORRUPT: - if( nArg==3 ){ - int opt = booleanValue(azArg[2]); - rc2 = sqlite3_test_control(testctrl, opt); - isOk = 3; - } - break; +COLLECT_HELP_TEXT[ + ".eval ?ARGS? Process each ARG's content as shell input.", + ".read FILE Read input from FILE", + " If FILE begins with \"|\", it is a command that generates the input.", + ".x ?OBJS or FLAGS? ... Excecute content of objects as shell input", + " FLAGS can be any of {-k -s -f} specifying what subsequent arguments are.", + " Arguments after -k are keys in a key/value table kept by the shell DB,", + " for which the object content to be executed is the corresponding value.", + " Arguments after -f name either files (or pipes), which are to be read", + " and the content executed. Input pipe names begin with '|'; the rest is", + " an OS-shell command which can be run by the OS shell to produce output.", + " Arguments after -s are strings, content of which is to be executed.", + " The default in effect for arguments prior to any FLAG is -k .", + " Arguments are executed in order until one fails.", +]; + +/* Return an allocated string with trailing whitespace trimmed except + * for a trailing newline. If empty (or OOM), return 0. Otherwise, the + * caller must eventually pass the return to sqlite3_free(). + */ +static char *zPrepForEval(const char *zVal, int ntc){ + int ixNewline = 0; + char c; + while( ntc>0 && IsSpace(c = zVal[ntc-1]) ){ + if( c=='\n' ) ixNewline = ntc-1; + --ntc; + } + if( ntc>0 ){ + /* The trailing newline (or some other placeholder) is important + * because one (or some other character) will likely be put in + * its place during process_input() line/group handling, along + * with a terminating NUL character. Without it, the NULL could + * land past the end of the allocation made just below. + */ + int nle = ixNewline>0; + return smprintf( "%.*s%s", ntc, zVal, &"\n"[nle] ); + }else{ + return 0; + } +} - /* sqlite3_test_control(sqlite3*) */ - case SQLITE_TESTCTRL_INTERNAL_FUNCTIONS: - rc2 = sqlite3_test_control(testctrl, p->db); - isOk = 3; - break; +/* Evaluate a string as input to the CLI. + * zName is the name to be given to the source for error reporting. + * Return usual dot command return codes as filtered by process_input(). + * No provision is made for error emission because, presumably, that + * has been done by whatever dot commands or SQL execution is invoked. + */ +static DotCmdRC shellEvalText(char *zIn, const char *zName, ShellExState *psx){ + DotCmdRC rv; + ShellInState *psi = ISS(psx); + InSource inRedir + = INSOURCE_STR_REDIR(zIn, zName, psi->pInSource); + psi->pInSource = &inRedir; + rv = process_input(psi); + psi->pInSource = inRedir.pFrom; + return rv; +} - case SQLITE_TESTCTRL_IMPOSTER: - if( nArg==5 ){ - rc2 = sqlite3_test_control(testctrl, p->db, - azArg[2], - integerValue(azArg[3]), - integerValue(azArg[4])); - isOk = 3; - } - break; +DISPATCHABLE_COMMAND( eval 3 1 0 ){ + DotCmdRC rv = DCR_Ok; + int ia = 1; + int nErrors = 0; + while( ia < nArg ){ + char *zA = azArg[ia++]; + int nc = strlen30(zA); + char *zSubmit = zPrepForEval(zA, nc); + if( zSubmit ){ + char zName[] = "eval arg[999]"; + sqlite3_snprintf(sizeof(zName), zName,"eval arg[%d]", ia-1); + rv = shellEvalText(zSubmit, zName, p); + sqlite3_free(zSubmit); + if( rvDCR_Error ) break; + } + } + } + rv |= (nErrors>0); + /* If error to be returned, indicate that complaining about it is done. */ + return (rv==DCR_Error)? DCR_CmdErred : rv; +} + +DISPATCHABLE_COMMAND( read 3 2 2 ){ + DotCmdRC rc = DCR_Ok; + FILE *inUse = 0; + int (*fCloser)(FILE *) = 0; + if( ISS(p)->bSafeMode ) return DCR_AbortError; + if( azArg[1][0]=='|' ){ +#ifdef SQLITE_OMIT_POPEN + *pzErr = smprintf("pipes are not supported in this OS\n"); + rc = DCR_Error; + /* p->out = STD_OUT; This was likely not needed. To be investigated. */ +#else + inUse = popen(azArg[1]+1, "r"); + if( inUse==0 ){ + *pzErr = smprintf("cannot open \"%s\"\n", azArg[1]); + rc = DCR_Error; + }else{ + fCloser = pclose; + } +#endif + }else if( (inUse = openChrSource(azArg[1]))==0 ){ + *pzErr = smprintf("cannot open \"%s\"\n", azArg[1]); + rc = DCR_Error; + }else{ + fCloser = fclose; + } + if( inUse!=0 ){ + InSource inSourceRedir + = INSOURCE_FILE_REDIR(inUse, azArg[1], ISS(p)->pInSource); + ISS(p)->pInSource = &inSourceRedir; + rc = process_input(ISS(p)); + /* If error(s) occured during process, leave complaining to them. */ + if( rc==DCR_Error ) rc = DCR_CmdErred; + assert(fCloser!=0); + fCloser(inUse); + ISS(p)->pInSource = inSourceRedir.pFrom; + } + return rc; +} - case SQLITE_TESTCTRL_SEEK_COUNT: { - u64 x = 0; - rc2 = sqlite3_test_control(testctrl, p->db, &x); - utf8_printf(p->out, "%llu\n", x); - isOk = 3; +DISPATCHABLE_COMMAND( x ? 1 0 ){ + int ia, nErrors = 0; + sqlite3_stmt *pStmt = 0; + sqlite3 *dbs = p->dbShell; + DotCmdRC rv = DCR_Ok; + enum { AsVar, AsString, AsFile } evalAs = AsVar; + + for( ia=1; ia 0 ){ + if( optionMatch(zOpt, azOpts[--io]) ){ + evalAs = io; + zOpt = 0; break; } - -#ifdef YYCOVERAGE - case SQLITE_TESTCTRL_PARSER_COVERAGE: { - if( nArg==2 ){ - sqlite3_test_control(testctrl, p->out); - isOk = 3; - } - break; + } + if( zOpt==0 ) continue; + } + switch( evalAs ){ + case AsVar: + if( pStmt==0 ){ + int rc; + if( dbs==0 || !shvars_table_exists(dbs) ){ + utf8_printf(STD_ERR, + "\".x vname\" can only be done after .var set ... .\n"); + return DCR_Error; } -#endif -#ifdef SQLITE_DEBUG - case SQLITE_TESTCTRL_TUNE: { - if( nArg==4 ){ - int id = (int)integerValue(azArg[2]); - int val = (int)integerValue(azArg[3]); - sqlite3_test_control(testctrl, id, &val); - isOk = 3; - }else if( nArg==3 ){ - int id = (int)integerValue(azArg[2]); - sqlite3_test_control(testctrl, -id, &rc2); - isOk = 1; - }else if( nArg==2 ){ - int id = 1; - while(1){ - int val = 0; - rc2 = sqlite3_test_control(testctrl, -id, &val); - if( rc2!=SQLITE_OK ) break; - if( id>1 ) utf8_printf(p->out, " "); - utf8_printf(p->out, "%d: %d", id, val); - id++; - } - if( id>1 ) utf8_printf(p->out, "\n"); - isOk = 3; - } - break; + rc = sqlite3_prepare_v2(dbs, "SELECT value FROM "SHVAR_TABLE_SNAME + " WHERE key=$1 AND uses="SPTU_Script, + -1, &pStmt, 0); + if( rc!=SQLITE_OK ){ + utf8_printf(STD_ERR, SHVAR_TABLE_SNAME" is wrongly created.\n"); + return DCR_Error; } -#endif - case SQLITE_TESTCTRL_SORTER_MMAP: - if( nArg==3 ){ - int opt = (unsigned int)integerValue(azArg[2]); - rc2 = sqlite3_test_control(testctrl, p->db, opt); - isOk = 3; + } + if( isalpha(azArg[ia][0]) ){ + int rc = sqlite3_reset(pStmt); + rc = sqlite3_bind_text(pStmt, 1, azArg[ia], -1, 0); + rc = sqlite3_step(pStmt); + if( rc==SQLITE_ROW ){ + ShellInState *psi = ISS(p); + const unsigned char *zValue = sqlite3_column_text(pStmt, 0); + int nb = sqlite3_column_bytes(pStmt, 0); + zSubmit = zPrepForEval((const char *)zValue, nb); + sqlite3_reset(pStmt); /* End the script read to unlock DB. */ + if( zSubmit ){ + rv = shellEvalText(zSubmit, azArg[ia], p); + sqlite3_free(zSubmit); + }else{ + continue; /* All white (or OOM), ignore. */ } - break; + }else{ + utf8_printf(STD_ERR, + "Skipping var '%s' (not set and executable.)\n", + azArg[ia]); + ++nErrors; + } + }else{ + utf8_printf(STD_ERR, + "Skipping badly named %s. Run \".help x\"\n", azArg[ia]); + ++nErrors; + } + break; + case AsString: + { + zSubmit = zPrepForEval(zOpt, strlen30(zOpt)); + if( zSubmit ){ + char zName[] = "x arg[999]"; + sqlite3_snprintf(sizeof(zName), zName,"x arg[%d]", ia); + rv = shellEvalText(zSubmit, zName, p); + sqlite3_free(zSubmit); + } + } + break; + case AsFile: + { + char *av[] = {"read", (char*)zOpt}; + rv = readCommand(av, ArraySize(av), p, pzErr); } + break; } - if( isOk==0 && iCtrl>=0 ){ - utf8_printf(p->out, "Usage: .testctrl %s %s\n", zCmd,aCtrl[iCtrl].zUsage); - rc = 1; - }else if( isOk==1 ){ - raw_printf(p->out, "%d\n", rc2); - }else if( isOk==2 ){ - raw_printf(p->out, "0x%08x\n", rc2); + if( rvDCR_Error ) break; + }else{ + ++nErrors; + rv = DCR_Error; } - }else -#endif /* !defined(SQLITE_UNTESTABLE) */ + } + sqlite3_finalize(pStmt); + rv |= (nErrors>0); + /* If error to be returned, indicate that complaining about it is done. */ + return (rv==DCR_Error)? DCR_CmdErred : rv; +} - if( c=='t' && n>4 && strncmp(azArg[0], "timeout", n)==0 ){ - open_db(p, 0); - sqlite3_busy_timeout(p->db, nArg>=2 ? (int)integerValue(azArg[1]) : 0); +/* End of published, standard dot-command implementation functions +COMMENT Build-time overrides of above dot-commands or new dot-commands may be +COMMENT incorporated into shell.c via: -it COMMAND_CUSTOMIZE= +COMMENT where names a file using the above methodology to +COMMENT define new or altered dot-commands and their help text. +*/ +INCLUDE( COMMAND_CUSTOMIZE ); + +COMMENT This help text is set seperately from dot-command definition section +COMMENT for the always-built-in, non-customizable commands with visible help. +COLLECT_HELP_TEXT[ + ".exit ?CODE? Exit this program with return-code CODE or 0", + ".quit Exit this program", +]; + +static void DotCommand_dtor(DotCommand *); +static const char * DotCommand_name(DotCommand *); +static const char * DotCommand_help(DotCommand *, const char *); +static DotCmdRC + DotCommand_argsCheck(DotCommand *, char **, int nArgs, char *azArgs[]); +static DotCmdRC + DotCommand_execute(DotCommand *, ShellExState *, char **, int, char *[]); + +static VTABLE_NAME(DotCommand) dot_cmd_VtabBuiltIn = { + DotCommand_dtor, + DotCommand_name, + DotCommand_help, + DotCommand_argsCheck, + DotCommand_execute +}; + +/* Define and populate command dispatch table. */ +static struct CommandInfo { + VTABLE_NAME(DotCommand) *mcVtabBuiltIn; + const char * cmdName; + DotCmdRC (*cmdDoer)(char *azArg[], int nArg, + ShellExState *, char **pzErr); + unsigned char minLen, minArgs, maxArgs; + const char *azHelp[2]; /* primary and secondary help text */ + void * pCmdData; + } command_table[] = { + COMMENT Emit the dispatch table entries generated and collected above. - #define DOT_CMD_INFO(cmd, nlenMin, minArgs, maxArgs, ht0, ht1 ) \ - { &dot_cmd_VtabBuiltIn, #cmd, cmd ## Command, \ - nlenMin, minArgs, maxArgs, { ht0, ht1 }, 0 \ - } ++#define DOT_CMD_INFO(cmd, nlenMin, minArgs, maxArgs) \ ++ &dot_cmd_VtabBuiltIn, #cmd, cmd ## Command, nlenMin, minArgs, maxArgs + EMIT_DOTCMD_INIT(2); +#undef DOT_CMD_INFO - { 0, 0, 0, 0, ~0, ~0, {0,0}, 0 } ++ { 0, 0, 0, 0, (u8)~0, (u8)~0, {0,0}, 0 } +}; +static unsigned numCommands + = sizeof(command_table)/sizeof(struct CommandInfo) - 1; + +static DotCommand *builtInCommand(int ix){ - if( ix<0 || ix>=numCommands ) return 0; ++ if( ix<0 || (unsigned)ix>=numCommands ) return 0; + return (DotCommand *)&command_table[ix]; +} + +static void DotCommand_dtor(DotCommand *pMe){ + UNUSED_PARAMETER(pMe); +} + +static const char * DotCommand_name(DotCommand *pMe){ + return ((struct CommandInfo *)pMe)->cmdName; +} + +static const char * DotCommand_help(DotCommand *pMe, const char * zWhat){ + struct CommandInfo *pci = (struct CommandInfo *)pMe; + if( zWhat==0 ) return pci->azHelp[0]; + if( *zWhat==0 ) return pci->azHelp[1]; + else return 0; +} + +static DotCmdRC + DotCommand_argsCheck(DotCommand *pMe, + char **pzErrMsg, int nArgs, char *azArgs[]){ + struct CommandInfo *pci = (struct CommandInfo *)pMe; + UNUSED_PARAMETER(azArgs); + if( pci->minArgs > nArgs ){ + if( pzErrMsg ){ + *pzErrMsg = smprintf("Too few arguments for \".%s\", need %d\n", + azArgs[0], pci->minArgs-1); + } + return DCR_TooFew; + }else if( pci->maxArgs > 0 && pci->maxArgs < nArgs ){ + if( pzErrMsg ){ + *pzErrMsg = smprintf("Too many arguments for \".%s\", over %d\n", + azArgs[0], pci->maxArgs-1); + } + return DCR_TooMany; + }else return DCR_Ok; +} + +static DotCmdRC + DotCommand_execute(DotCommand *pMe, ShellExState *pssx, + char **pzErrMsg, int nArgs, char *azArgs[]){ + return (((struct CommandInfo *)pMe)->cmdDoer)(azArgs, nArgs, pssx, pzErrMsg); +} + +/***************** +** DotCommand iteration by name match, used by the .help dot-command. +** DotCommands, or their ad-hoc stand-ins, having matching names are produced +** in lexical order, with the iterator indicating which has been produced. +** If .zAdhocHelpName == 0, it is a regular DotCommand. Otherwise, the +** ".unknown" DotCommand is returned, whose help() method is to be used. +** Any returned CmdMatchIter must eventually be passed to freeCmdMatchIter(). +*/ +typedef struct CmdMatchIter { + ShellExState *psx; + /* 0 indicates prepared statement; non-0 is the glob pattern. */ + const char *zPattern; + union { + DotCommand *pDotCmd; +#if SHELL_DYNAMIC_EXTENSION + sqlite3_stmt *stmt; +#endif - }; ++ } cursor; +#if SHELL_DYNAMIC_EXTENSION + char *zAdhocHelpText; /* registered extension ad-hoc help */ +#endif +} CmdMatchIter; + +/* Release resources held by the iterator and clear it. */ +static void freeCmdMatchIter(CmdMatchIter *pMMI){ + if( pMMI->zPattern!=0 ){ + sqlite3_free((void *)pMMI->zPattern); + pMMI->zPattern = 0; - pMMI->pDotCmd = 0; ++ pMMI->cursor.pDotCmd = 0; + } +#if SHELL_DYNAMIC_EXTENSION + else{ - sqlite3_finalize(pMMI->stmt); - pMMI->stmt = 0; ++ sqlite3_finalize(pMMI->cursor.stmt); ++ pMMI->cursor.stmt = 0; + } + sqlite3_free(pMMI->zAdhocHelpText); + pMMI->zAdhocHelpText = 0; +#endif +} + +/* Prepare an iterator that will produce a sequence of DotCommand + * pointers whose referents' names match the given cmdFragment. + * Return how many will match (if iterated upon return.) */ +static int findMatchingDotCmds(const char *cmdFragment, + CmdMatchIter *pMMI, + ShellExState *psx){ + CmdMatchIter mmi = { psx, 0, 0 }; + int rv = 0; +#if SHELL_DYNAMIC_EXTENSION + if( ISS(psx)->bDbDispatch ){ + sqlite3_stmt *stmtCount = 0; + /* Prepare rv.stmt to yield results glob-matching cmdFragment. */ + static const char * const zSqlIter = + "SELECT name, extIx, cmdIx, help FROM "SHELL_HELP_VIEW" " + "WHERE name glob (?||'*') ORDER BY name"; + static const char * const zSqlCount = + "SELECT count(*) FROM "SHELL_HELP_VIEW" " + "WHERE name glob (?||'*')"; + if( pMMI ){ - sqlite3_prepare_v2(psx->dbShell, zSqlIter, -1, &mmi.stmt, 0); - sqlite3_bind_text(mmi.stmt, 1, cmdFragment? cmdFragment : "", -1, 0); ++ sqlite3_prepare_v2(psx->dbShell, zSqlIter, -1, &mmi.cursor.stmt, 0); ++ sqlite3_bind_text(mmi.cursor.stmt, 1, cmdFragment? cmdFragment:"", -1, 0); + } + sqlite3_prepare_v2(psx->dbShell, zSqlCount, -1, &stmtCount, 0); + sqlite3_bind_text(stmtCount, 1, cmdFragment? cmdFragment : "", -1, 0); + if( SQLITE_ROW==sqlite3_step(stmtCount) ){ + rv = sqlite3_column_int(stmtCount, 0); + }else assert(0); + sqlite3_finalize(stmtCount); }else +#endif + { + int i = 0; + mmi.zPattern = smprintf("%s*", cmdFragment? cmdFragment : ""); + shell_check_oom((void *)mmi.zPattern); - if( c=='t' && n>=5 && strncmp(azArg[0], "timer", n)==0 ){ - if( nArg==2 ){ - enableTimer = booleanValue(azArg[1]); - if( enableTimer && !HAS_TIMER ){ - raw_printf(stderr, "Error: timer not available on this system.\n"); - enableTimer = 0; - } - }else{ - raw_printf(stderr, "Usage: .timer on|off\n"); - rc = 1; + struct CommandInfo *pCI = command_table; - mmi.pDotCmd = (DotCommand *)command_table; ++ mmi.cursor.pDotCmd = (DotCommand *)command_table; + while( pCIcmdName)==0 ) ++rv; + ++pCI; } - }else + } + if( pMMI ) *pMMI = mmi; + else freeCmdMatchIter(&mmi); + return rv; +} -#ifndef SQLITE_OMIT_TRACE - if( c=='t' && strncmp(azArg[0], "trace", n)==0 ){ - int mType = 0; - int jj; - open_db(p, 0); - for(jj=1; jjeTraceType = SHELL_TRACE_EXPANDED; - } -#ifdef SQLITE_ENABLE_NORMALIZE - else if( optionMatch(z, "normalized") ){ - p->eTraceType = SHELL_TRACE_NORMALIZED; - } -#endif - else if( optionMatch(z, "plain") ){ - p->eTraceType = SHELL_TRACE_PLAIN; - } - else if( optionMatch(z, "profile") ){ - mType |= SQLITE_TRACE_PROFILE; - } - else if( optionMatch(z, "row") ){ - mType |= SQLITE_TRACE_ROW; - } - else if( optionMatch(z, "stmt") ){ - mType |= SQLITE_TRACE_STMT; - } - else if( optionMatch(z, "close") ){ - mType |= SQLITE_TRACE_CLOSE; - } - else { - raw_printf(stderr, "Unknown option \"%s\" on \".trace\"\n", z); - rc = 1; - goto meta_command_exit; - } +/* Produce the next DotCommand pointer from the iterator, or 0 if no next. */ +static DotCommand * nextMatchingDotCmd(CmdMatchIter *pMMI){ + DotCommand *rv = 0; +#if SHELL_DYNAMIC_EXTENSION + if( pMMI->zPattern==0 ){ - int rc = sqlite3_step(pMMI->stmt); ++ int rc = sqlite3_step(pMMI->cursor.stmt); + if( rc==SQLITE_ROW ){ + /* name, extIx, cmdIx, help */ - int extIx = sqlite3_column_int(pMMI->stmt, 1); - int cmdIx = sqlite3_column_int(pMMI->stmt, 2); ++ int extIx = sqlite3_column_int(pMMI->cursor.stmt, 1); ++ int cmdIx = sqlite3_column_int(pMMI->cursor.stmt, 2); + ShellInState *psi = ISS(pMMI->psx); + sqlite3_free(pMMI->zAdhocHelpText); + if( cmdIx>=0 ){ + pMMI->zAdhocHelpText = 0; + return command_by_index(psi, extIx, cmdIx); }else{ - const unsigned char *zHT = sqlite3_column_text(pMMI->stmt, 3); - output_file_close(p->traceOut); - p->traceOut = output_file_open(azArg[1], 0); ++ const unsigned char *zHT = sqlite3_column_text(pMMI->cursor.stmt, 3); + assert(psi->pUnknown!=0); + assert(extIxnumExtLoaded && extIx>0); - if( zHT==0 ) zHT = sqlite3_column_text(pMMI->stmt, 0); ++ if( zHT==0 ) zHT = sqlite3_column_text(pMMI->cursor.stmt, 0); + pMMI->zAdhocHelpText = sqlite3_mprintf("%s", zHT); + return psi->pShxLoaded[extIx].pUnknown; } - } - if( p->traceOut==0 ){ - sqlite3_trace_v2(p->db, 0, 0, 0); }else{ - sqlite3_finalize(pMMI->stmt); - pMMI->stmt = 0; - if( mType==0 ) mType = SQLITE_TRACE_STMT; - sqlite3_trace_v2(p->db, mType, sql_trace_callback, p); ++ sqlite3_finalize(pMMI->cursor.stmt); ++ pMMI->cursor.stmt = 0; } }else -#endif /* !defined(SQLITE_OMIT_TRACE) */ - -#if defined(SQLITE_DEBUG) && !defined(SQLITE_OMIT_VIRTUALTABLE) - if( c=='u' && strncmp(azArg[0], "unmodule", n)==0 ){ - int ii; - int lenOpt; - char *zOpt; - if( nArg<2 ){ - raw_printf(stderr, "Usage: .unmodule [--allexcept] NAME ...\n"); - rc = 1; - goto meta_command_exit; - } - open_db(p, 0); - zOpt = azArg[1]; - if( zOpt[0]=='-' && zOpt[1]=='-' && zOpt[2]!=0 ) zOpt++; - lenOpt = (int)strlen(zOpt); - if( lenOpt>=3 && strncmp(zOpt, "-allexcept",lenOpt)==0 ){ - assert( azArg[nArg]==0 ); - sqlite3_drop_modules(p->db, nArg>2 ? (const char**)(azArg+2) : 0); - }else{ - for(ii=1; iidb, azArg[ii], 0, 0); +#endif + { - struct CommandInfo *pCI = (struct CommandInfo *)(pMMI->pDotCmd); ++ struct CommandInfo *pCI = (struct CommandInfo *)(pMMI->cursor.pDotCmd); + assert(pCI>=command_table && pCI<=command_table+numCommands); + while( pCIzPattern, pCI->cmdName)==0 ) rv = pMMI->pDotCmd; - pMMI->pDotCmd = (DotCommand *)(++pCI); ++ if( sqlite3_strglob(pMMI->zPattern, pCI->cmdName)==0 ){ ++ rv = pMMI->cursor.pDotCmd; + } ++ pMMI->cursor.pDotCmd = (DotCommand *)(++pCI); + if( rv!=0 ) break; } + } + return rv; +} + +/***************** +** DotCommand lookup +** +** For the non-extended or non-extensible shell, this function does +** a binary search of the fixed list of dot-command info structs. +** For an extended shell, it queries the shell's DB. Either way, +** this function returns a DotCommand pointer if one can be found +** with an adequate match for the given name. Here, "adequate" may +** vary according to whether shell extensions have been loaded. If +** not, the match must be for as many characters as set within the +** above CommandInfo array (set via DISPATCHABLE_COMMAND macro call.) +** If shell extensions are loaded, the match must be long enough to +** result in a unique lookup. +*/ +DotCommand *findDotCommand(const char *cmdName, ShellExState *psx, + /* out */ int *pnFound){ + if( pnFound ) *pnFound = 0; +#if SHELL_DYNAMIC_EXTENSION + if( ISS(psx)->bDbDispatch ){ + int rc; + int extIx = -1, cmdIx = -1, nf = 0; + sqlite3_stmt *pStmt = 0; + const char *zSql = "SELECT COUNT(*), extIx, cmdIx" + " FROM "SHELL_DISP_VIEW" WHERE name glob (?||'*')"; + rc = sqlite3_prepare_v2(psx->dbShell, zSql, -1, &pStmt, 0); + sqlite3_bind_text(pStmt, 1, cmdName, -1, 0); + rc = sqlite3_step(pStmt); + nf = sqlite3_column_int(pStmt, 0); + extIx = sqlite3_column_int(pStmt, 1); + cmdIx = sqlite3_column_int(pStmt, 2); + sqlite3_finalize(pStmt); + if( rc!= SQLITE_ROW ) return 0; + if( pnFound ) *pnFound = nf; + if( nf!=1 ) return 0; /* Future: indicate collisions if > 1 */ + return (cmdIx<0)? 0 : command_by_index(ISS(psx), extIx, cmdIx); }else #endif + { + int cmdLen = strlen30(cmdName); + struct CommandInfo *pci = 0; + int ixb = 0, ixe = numCommands-1; + while( ixb <= ixe ){ + int ixm = (ixb+ixe)/2; + int md = strncmp(cmdName, command_table[ixm].cmdName, cmdLen); + if( md>0 ){ + ixb = ixm+1; + }else if( md<0 ){ + ixe = ixm-1; + }else{ + /* Have a match, see whether it's ambiguous. */ + if( command_table[ixm].minLen > cmdLen ){ + if( (ixm>0 + && !strncmp(cmdName, command_table[ixm-1].cmdName, cmdLen)) + || + (ixm1 suffices */ + return 0; + } + } + pci = &command_table[ixm]; + if( pnFound ) *pnFound = 1; + break; + } + } + if( pnFound && pci ) *pnFound = 1; + return (DotCommand *)pci; + } +} -#if SQLITE_USER_AUTHENTICATION - if( c=='u' && strncmp(azArg[0], "user", n)==0 ){ - if( nArg<2 ){ - raw_printf(stderr, "Usage: .user SUBCOMMAND ...\n"); +/* +** Given a DotCommand, desired help level, +** ( possibly retreived ad-hoc help text for extensible shell, ) +** and an optional all-text search pattern, then +** when level==0 and primary help available, output it +** when level==1 and primary or secondary help available, output it +** when level==2 and any help text matches pattern, output it +** when level>2 or no pattern: output all help text +** If cLead==0, anything meeting above criteria is output. Otherwise, output +** is restricted to those commands whose primary help begins with cLead. +** Return 1 if anything output, else 0. +*/ +static int putSelectedCmdHelp(DotCommand *pmc, int iLevel, char cLead, +#if SHELL_DYNAMIC_EXTENSION + const char *zHelpText, +#endif + FILE *out, const char *zSearch){ + int rc = 0; + assert(pmc!=0); +#if SHELL_DYNAMIC_EXTENSION + if( zHelpText ){ + const char *zHT = zHelpText+1; /* skip over classifier */ + if( cLead && *zHelpText!= cLead ) return 0; + if( *zHelpText==0 ) return 0; + const char *zLE = zHT; + switch( iLevel ){ + case 0: + while( *zLE && *zLE++!='\n' ) {} + utf8_printf(out,".%.*s", (int)(zLE-zHT), zHT); + rc = 1; + break; + case 2: + if( zSearch ){ + if( !sqlite3_strlike(zSearch, zHT, 0) ) break; + } + /* else fall thru */ + case 1: + default: + utf8_printf(out,".%s", zHT); rc = 1; - goto meta_command_exit; } - open_db(p, 0); - if( strcmp(azArg[1],"login")==0 ){ - if( nArg!=4 ){ - raw_printf(stderr, "Usage: .user login USER PASSWORD\n"); - rc = 1; - goto meta_command_exit; - } - rc = sqlite3_user_authenticate(p->db, azArg[2], azArg[3], - strlen30(azArg[3])); - if( rc ){ - utf8_printf(stderr, "Authentication failed for user %s\n", azArg[2]); - rc = 1; - } - }else if( strcmp(azArg[1],"add")==0 ){ - if( nArg!=5 ){ - raw_printf(stderr, "Usage: .user add USER PASSWORD ISADMIN\n"); + }else +#endif + { + const char *zHTp = pmc->pMethods->help(pmc, 0); + const char *zHTs = pmc->pMethods->help(pmc, ""); + if( !zHTp && !zHTs ) return 0; + if( cLead && zHTp && *zHTp!= cLead ) return 0; + switch( iLevel ){ + case 0: + case 1: + if( zHTp ){ + utf8_printf(out, HELP_TEXT_FMTP, zHTp+1); rc = 1; - goto meta_command_exit; } - rc = sqlite3_user_add(p->db, azArg[2], azArg[3], strlen30(azArg[3]), - booleanValue(azArg[4])); - if( rc ){ - raw_printf(stderr, "User-Add failed: %d\n", rc); + if( iLevel>0 && zHTs ){ + utf8_printf(out, HELP_TEXT_FMTS, zHTs); rc = 1; } - }else if( strcmp(azArg[1],"edit")==0 ){ - if( nArg!=5 ){ - raw_printf(stderr, "Usage: .user edit USER PASSWORD ISADMIN\n"); - rc = 1; - goto meta_command_exit; + break; + case 2: + if( zSearch ){ + int m = 0; + if( zHTp && !sqlite3_strlike(zSearch, zHTp, 0) ) ++m; + if( zHTs && !sqlite3_strlike(zSearch, zHTs, 0) ) ++m; + if( m==0 ) break; + } + /* else fall thru */ + default: + if( zHTp ) utf8_printf(out, HELP_TEXT_FMTP, zHTp+1); + if( zHTs ) utf8_printf(out, HELP_TEXT_FMTS, zHTs); + rc = 1; + } + } + return rc; +} + +/* +** Output primary (single-line) help for a known command. +*/ +static void showPrimaryHelp(FILE *out, const char *zCmd, ShellExState *psx){ + CmdMatchIter cmi = {0}; + int nm = findMatchingDotCmds(zCmd, &cmi, psx); + DotCommand *pdc = nextMatchingDotCmd(&cmi); + if( pdc!=0 ){ + putSelectedCmdHelp(pdc, 0, 0, +#if SHELL_DYNAMIC_EXTENSION + cmi.zAdhocHelpText, +#endif + out, 0); + } + freeCmdMatchIter(&cmi); +} + +/* +** Output various subsets of help text. These 6 are defined: +** 1. HO_AllP For all commands, primary help text only. +** 2. HO_AllX For all commands, complete help text. +** 3. HO_LikeP For multiple commands matching pattern, primary help text only. +** 4. HO_OneX For a single matched command, complete help text. +** 5. HO_LikeT For commands whose help contains a pattern, complete help text. +** 6. HO_Undoc For all internal "undocumented" (without normal help) commands. +** These variations are indicated thusly: +** 1. zPattern is NULL +** 2. zPattern is "" +** 3. zPattern is a prefix matching more than one command +** 4. zPattern is a word or prefix matching just one command +** 5. zPattern is neither case 3 or 4 but is found in complete help text +** 6. zPattern is exactly the pointer known locally as zHelpAll. +** +** Return the number of matches. +*/ +static int showHelp(FILE *out, const char *zPattern, ShellExState *psx){ + u8 bNullPattern = zPattern==0; + u8 bShowUndoc = zPattern==zHelpAll; + u8 bEmptyPattern = !bNullPattern && (*zPattern==0 || bShowUndoc); + int npm = 0; /* track how many matches found */ + CmdMatchIter cmi = {0}; + DotCommand *pdc; + char *zPat = 0; + char cLead = (bShowUndoc)? ',' : '.'; + int iLevel = 0; + enum { + HO_Tbd, HO_AllP, HO_AllX, HO_LikeP, HO_OneX, HO_LikeT, HO_Undoc + } hoKind = bShowUndoc? HO_Undoc : HO_Tbd; + + if( hoKind==HO_Undoc ){ - int ixct = 0; ++ unsigned ixct = 0; + utf8_printf(out, "%s\n%s\n", + "The following commands are for SQLite diagnosis and internal testing.", + "They are undocumented and subject to change without notice."); + /* Bypass command lookup/resolution. This is just for internal commands. */ + while( ixctazHelp[0]; + if( zH && *zH==cLead ){ + utf8_printf(out, HELP_TEXT_FMTP, zH+1); + zH = pci->azHelp[1]; + if( zH ) utf8_printf(out, HELP_TEXT_FMTS, zH); + ++npm; + } + ++ixct; + } + return npm; + } + npm = findMatchingDotCmds(zPattern, &cmi, psx); + if( bNullPattern ) hoKind = HO_AllP; + else if( bEmptyPattern ) hoKind = HO_AllX; + else if( npm>1 ) hoKind = HO_LikeP; + else if( npm==1 ) hoKind = HO_OneX; + else{ + hoKind = HO_LikeT; + zPat = smprintf("%%%s%%", zPattern); + shell_check_oom(zPat); + } + zPattern = 0; + iLevel = 1; + switch( hoKind ){ + case HO_AllP: case HO_LikeP: + iLevel = 0; + break; + case HO_AllX: + break; + case HO_OneX: + cLead = 0; + break; + case HO_LikeT: + zPattern = zPat; + break; + default: return 0; + } + npm = 0; + while( 0 != (pdc = nextMatchingDotCmd(&cmi)) ){ + npm += putSelectedCmdHelp(pdc, iLevel, cLead, +#if SHELL_DYNAMIC_EXTENSION + cmi.zAdhocHelpText, +#endif + out, zPattern); + } + freeCmdMatchIter(&cmi); + sqlite3_free(zPat); + return npm; +} + +/* Perform preparation needed prior to actually running any dot command. */ +static void command_prep(ShellInState *psi){ + clearTempFile(psi); +#ifndef SQLITE_OMIT_VIRTUALTABLE + if( psi->expert.pExpert ){ + expertFinish(psi, 1, 0); + } +#endif +} + +/* Perform post-execution actions needed after running any dot command. */ +static void command_post(ShellInState *psi){ + if( psi->outCount ){ + psi->outCount--; + if( psi->outCount==0 ) output_reset(psi); + } + updateSafeMode(psi); +} + +/* Issue errors per returned DotCmdRC and error message, handle certain + * exceptional returns, and translate return to the sanitized first 8. + */ +static DotCmdRC dot_command_errors(char *zErr, char *azArg[], int nArg, + DotCmdRC dcr, ShellExState *psx){ + if( psx->shellAbruptExit!=0 ){ + if( psx->shellAbruptExit>0x1ff ) dcr = DCR_AbortError; + else dcr = DCR_Exit | (dcr & DCR_Error); + } + if( dcr==DCR_CmdErred ){ + /* Error message(s) already emitted. Just translate to execute error. */ + dcr = DCR_Error; + }else if( dcr==DCR_SayUsage ){ + if( zErr ){ + utf8_printf(STD_ERR, "%s", zErr); + }else{ + utf8_printf(STD_ERR, "Usage:\n"); + showPrimaryHelp(STD_ERR, azArg[0], psx); + } + dcr = DCR_Error; + }else if( dcr > DCR_AbortError ){ + /* Handle invocation errors. */ + int ia = dcr & DCR_ArgIxMask; + const char *pArg1st = (ia>=nArg)? "" : azArg[ia]; + const char *pArg2nd = 0; + int ec = dcr & ~DCR_ArgIxMask; + int unknownCmd = 0; + const char *z = 0; + switch( ec ){ + case DCR_Unknown: + unknownCmd = ia==0; + if( unknownCmd ) z = "unknown dot command: \".%s\""; + else{ + z = "On .%s, unknown option or subcommand: \"%s\""; + pArg2nd = pArg1st; + pArg1st = azArg[0]; } - rc = sqlite3_user_change(p->db, azArg[2], azArg[3], strlen30(azArg[3]), - booleanValue(azArg[4])); - if( rc ){ - raw_printf(stderr, "User-Edit failed: %d\n", rc); - rc = 1; + break; + case DCR_Ambiguous: + if( ia>0 ){ + z = "For .%s, \"%s\" is ambiguous; it matches more than one subcommand"; + pArg2nd = pArg1st; + pArg1st = azArg[0]; + }else z = "\"%s\" is ambiguous; it matches multiple dot commands"; + break; + case DCR_Unpaired: + z = "With .%s, option %s must be paired with a value argument"; + pArg2nd = pArg1st; + pArg1st = azArg[0]; + break; + case DCR_TooMany: + z = "Excess arguments provided; try .help %s"; + pArg1st = azArg[0]; + break; + case DCR_TooFew: + z = "Insufficient arguments; try .help %s"; + pArg1st = azArg[0]; + break; + case DCR_Missing: + z = "Required arguments missing; try .help %s"; + pArg1st = azArg[0]; + break; + case DCR_ArgWrong: + z = "Argument(s) incorrect; try .help %s"; + pArg1st = azArg[0]; + break; + default: + z = "? Bizarre return from %s"; + break; + } + if( !zErr ){ + if( z!=0 ){ + char *ze = smprintf("Error: %z\n", smprintf(z, pArg1st, pArg2nd)); + utf8_printf(STD_ERR, "%s", ze); + sqlite3_free(ze); } - }else if( strcmp(azArg[1],"delete")==0 ){ - if( nArg!=3 ){ - raw_printf(stderr, "Usage: .user delete USER\n"); - rc = 1; - goto meta_command_exit; + }else{ + const char *fmt + = (sqlite3_strnicmp(zErr, "Error", 5)==0)? "%s" : "Error: %s"; + utf8_printf(STD_ERR, fmt, zErr); + } + if( INSOURCE_IS_INTERACTIVE(ISS(psx)->pInSource) ){ + if( unknownCmd ){ + utf8_printf(STD_ERR, "Enter \".help\" for a list of commands.\n"); + }else{ + utf8_printf(STD_ERR, "Usage:\n"); + showPrimaryHelp(STD_ERR, azArg[0], psx); } - rc = sqlite3_user_delete(p->db, azArg[2]); - if( rc ){ - raw_printf(stderr, "User-Delete failed: %d\n", rc); - rc = 1; + } + /* If the shell DB becomes messed up, at least .quit will be doable. */ + if( unknownCmd && psx->dbShell!=0 + && sqlite3_strnicmp(azArg[0],"quit",strlen30(azArg[0]))==0 ){ + dcr = DCR_Return; + }else{ + dcr = DCR_Error; + } + }else{ + /* Handle execution errors. */ + if( dcr==DCR_AbortError ){ + if( zErr ){ + utf8_printf(STD_ERR, "Error: \".%s\" may not %s in -safe mode\n", + azArg[0], zErr); + }else { + utf8_printf(STD_ERR, "Error: \".%s\" forbidden in -safe mode\n", + azArg[0]); } + psx->shellAbruptExit = 0x203; }else{ - raw_printf(stderr, "Usage: .user login|add|edit|delete ...\n"); - rc = 1; - goto meta_command_exit; + int error = dcr & DCR_Error; + int action = dcr & ~DCR_Error; + if( error ){ + if( zErr ){ + const char *fmt + = (sqlite3_strnicmp(zErr, "Error", 5)==0)? "%s" : "Error: %s"; + utf8_printf(STD_ERR, fmt, zErr); + } + else utf8_printf(STD_ERR, "Error: .%s failed\n", azArg[0]); + } } - }else -#endif /* SQLITE_USER_AUTHENTICATION */ + } + return dcr; +} - if( c=='v' && strncmp(azArg[0], "version", n)==0 ){ - utf8_printf(p->out, "SQLite %s %s\n" /*extra-version-info*/, - sqlite3_libversion(), sqlite3_sourceid()); -#if SQLITE_HAVE_ZLIB - utf8_printf(p->out, "zlib version %s\n", zlibVersion()); -#endif -#define CTIMEOPT_VAL_(opt) #opt -#define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt) -#if defined(__clang__) && defined(__clang_major__) - utf8_printf(p->out, "clang-" CTIMEOPT_VAL(__clang_major__) "." - CTIMEOPT_VAL(__clang_minor__) "." - CTIMEOPT_VAL(__clang_patchlevel__) "\n"); -#elif defined(_MSC_VER) - utf8_printf(p->out, "msvc-" CTIMEOPT_VAL(_MSC_VER) "\n"); -#elif defined(__GNUC__) && defined(__VERSION__) - utf8_printf(p->out, "gcc-" __VERSION__ "\n"); +/* Argument-check and execute a found DotCommand, wrapping execution + * with command_{prep,post}(...), and issue errors as made evident. + * Return one of the "Post-execute action and success/error status" + * codes from the DotCmdRC enum. + * + * Note that this function is exposed for shell extensions to use. + * + * This should be called for "top-level" dot command execution only. + * Should an extension wrap or use a DotCommand object to effect its + * own functionality, that object's execute() method should be called + * directly, without going through this function. + */ +static DotCmdRC runDotCommand(DotCommand *pmc, char *azArg[], int nArg, + ShellExState *psx){ + char *zErr = 0; + DotCmdRC dcr = pmc->pMethods->argsCheck(pmc, &zErr, nArg, azArg); + + command_prep(ISS(psx)); + if( dcr==DCR_Ok ){ + dcr = pmc->pMethods->execute(pmc, psx, &zErr, nArg, azArg); + } + if( dcr!=DCR_Ok ){ + dcr = dot_command_errors(zErr, azArg, nArg, dcr, psx); + } + sqlite3_free(zErr); + command_post(ISS(psx)); + return dcr; +} + +/* +** If an input line or line group begins with "." then invoke this routine +** to process that line. +** +** Returns sanitized DotCmdRC values, with invocation error codes +** translated to DCR_Error, so that only these 8 returns are possible: +** DCR_Ok, DCR_Return, DCR_Exit or DCR_Abort possibly or'ed with DCR_Error. +** +** Any applicable error messages are issued along with output messages. +*/ +static DotCmdRC do_dot_command(char *zLine, ShellExState *psx){ + int h = 1; /* Passing over leading '.' */ + int nArg = 0; + char *azArg[52]; + DotCmdRC dcr = DCR_Ok; +#if SHELL_VARIABLE_EXPANSION + int ncLineIn = strlen30(zLine); + u8 bExpVars = SHEXT_VAREXP(ISS(psx)); #endif - }else - if( c=='v' && strncmp(azArg[0], "vfsinfo", n)==0 ){ - const char *zDbName = nArg==2 ? azArg[1] : "main"; - sqlite3_vfs *pVfs = 0; - if( p->db ){ - sqlite3_file_control(p->db, zDbName, SQLITE_FCNTL_VFS_POINTER, &pVfs); - if( pVfs ){ - utf8_printf(p->out, "vfs.zName = \"%s\"\n", pVfs->zName); - raw_printf(p->out, "vfs.iVersion = %d\n", pVfs->iVersion); - raw_printf(p->out, "vfs.szOsFile = %d\n", pVfs->szOsFile); - raw_printf(p->out, "vfs.mxPathname = %d\n", pVfs->mxPathname); + /* Parse the input line into tokens which are 0-terminated and left in-place. + */ + while( zLine[h] && nArgdb ){ - sqlite3_file_control(p->db, "main", SQLITE_FCNTL_VFS_POINTER, &pCurrent); - } - for(pVfs=sqlite3_vfs_find(0); pVfs; pVfs=pVfs->pNext){ - utf8_printf(p->out, "vfs.zName = \"%s\"%s\n", pVfs->zName, - pVfs==pCurrent ? " <--- CURRENT" : ""); - raw_printf(p->out, "vfs.iVersion = %d\n", pVfs->iVersion); - raw_printf(p->out, "vfs.szOsFile = %d\n", pVfs->szOsFile); - raw_printf(p->out, "vfs.mxPathname = %d\n", pVfs->mxPathname); - if( pVfs->pNext ){ - raw_printf(p->out, "-----------------------------------\n"); + 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; + resolve_backslashes(azArg[nArg-1]); } - }else + } + azArg[nArg] = 0; /* No code here relies on this, but some extension might. */ - if( c=='v' && strncmp(azArg[0], "vfsname", n)==0 ){ - const char *zDbName = nArg==2 ? azArg[1] : "main"; - char *zVfsName = 0; - if( p->db ){ - sqlite3_file_control(p->db, zDbName, SQLITE_FCNTL_VFSNAME, &zVfsName); - if( zVfsName ){ - utf8_printf(p->out, "%s\n", zVfsName); - sqlite3_free(zVfsName); + /* Process the input line. If it was empty, do nothing and declare success. + * Note that "empty" includes a leading '.' followed by nothing else. + */ + if( nArg>0 ){ + int nFound; + DotCommand *pmc = findDotCommand(azArg[0], psx, &nFound); + if( pmc==0 || nFound>1 ){ + if( nFound==0 ){ + dcr = DCR_Unknown; +#if SHELL_DYNAMIC_EXTENSION + pmc = ISS(psx)->pUnknown; + if( pmc ) dcr = runDotCommand(pmc, azArg, nArg, psx); +#endif + }else{ + dcr = DCR_Ambiguous; + } + if( dcr > DCR_ArgIxMask ){ + /* Issue error for unknown or inadequately specified dot command. */ + dcr = dot_command_errors(0, azArg, nArg, dcr, psx); } } - }else - - if( c=='w' && strncmp(azArg[0], "wheretrace", n)==0 ){ - unsigned int x = nArg>=2 ? (unsigned int)integerValue(azArg[1]) : 0xffffffff; - sqlite3_test_control(SQLITE_TESTCTRL_TRACEFLAGS, 3, &x); - }else - - if( c=='w' && strncmp(azArg[0], "width", n)==0 ){ - int j; - assert( nArg<=ArraySize(azArg) ); - p->nWidth = nArg-1; - p->colWidth = realloc(p->colWidth, (p->nWidth+1)*sizeof(int)*2); - if( p->colWidth==0 && p->nWidth>0 ) shell_out_of_memory(); - if( p->nWidth ) p->actualWidth = &p->colWidth[p->nWidth]; - for(j=1; jcolWidth[j-1] = (int)integerValue(azArg[j]); + else{ + char *arg0 = azArg[0]; + azArg[0] = (char *)(pmc->pMethods->name(pmc)); + /* Run found command and issue or handle any errors it may report. */ + dcr = runDotCommand(pmc, azArg, nArg, psx); + azArg[0] = arg0; } - }else - - { - utf8_printf(stderr, "Error: unknown command or invalid arguments: " - " \"%s\". Enter \".help\" for help\n", azArg[0]); - rc = 1; } -meta_command_exit: - if( p->outCount ){ - p->outCount--; - if( p->outCount==0 ) output_reset(p); +#if SHELL_VARIABLE_EXPANSION + if( bExpVars ){ + /* Free any arguments that are allocated rather than tokenized in place. */ + for( n=1; n0 && iArgOffsetbSafeMode = p->bSafeModePersist; - return rc; +#endif + return dcr; } /* Line scan result and intermediate states (supporting scan resumption) @@@ -15834,66 -12364,52 +15836,66 @@@ int SQLITE_CDECL SHELL_MAIN(int argc, w */ if( stdin_is_interactive ){ char *zHome; -- char *zHistory; - int nHistory; - printf( - "SQLite version %s %.19s\n" /*extra-version-info*/ - "Enter \".help\" for usage hints.\n", - sqlite3_libversion(), sqlite3_sourceid() - ); - if( warnInmemoryDb ){ - printf("Connected to a "); - printBold("transient in-memory database"); - printf(".\nUse \".open FILENAME\" to reopen on a " - "persistent database.\n"); - } - zHistory = getenv("SQLITE_HISTORY"); - if( zHistory ){ - zHistory = strdup(zHistory); - }else if( (zHome = find_home_dir(0))!=0 ){ - nHistory = strlen30(zHome) + 20; - if( (zHistory = malloc(nHistory))!=0 ){ - sqlite3_snprintf(nHistory, zHistory,"%s/.sqlite_history", zHome); ++ char *zHistory = 0; + if( bQuiet ){ + /* bQuiet is almost like normal interactive, but quieter. */ + mainPrompt[0] = 0; + continuePrompt[0] = 0; + }else{ + fprintf(STD_OUT, + "SQLite version %s %.19s\n" /*extra-version-info*/ + "Enter \".help\" for usage hints.\n", + sqlite3_libversion(), sqlite3_sourceid() + ); + if( warnInmemoryDb ){ + fprintf(STD_OUT, "Connected to a "); + printBold("transient in-memory database"); + fprintf(STD_OUT, ".\nUse \".open FILENAME\" to reopen on a " + "persistent database.\n"); } - } - if( zHistory ){ shell_read_history(zHistory); } + zHistory = getenv("SQLITE_HISTORY"); + if( zHistory ){ + zHistory = strdup(zHistory); + }else if( (zHome = find_home_dir(0))!=0 ){ + int nHistory = strlen30(zHome) + 20; + if( (zHistory = malloc(nHistory))!=0 ){ + sqlite3_snprintf(nHistory, zHistory,"%s/.sqlite_history", zHome); + } + } + if( zHistory ){ shell_read_history(zHistory); } #if HAVE_READLINE || HAVE_EDITLINE - rl_attempted_completion_function = readline_completion; + rl_attempted_completion_function = readline_completion; #elif HAVE_LINENOISE - linenoiseSetCompletionCallback(linenoise_completion); + linenoiseSetCompletionCallback(linenoise_completion); #endif - data.in = 0; - rc = process_input(&data); - if( zHistory ){ - shell_stifle_history(2000); - shell_write_history(zHistory); - free(zHistory); + } + data.pInSource = &termInSource; /* read from stdin interactively */ + drc = process_input(&data); + rc = (drc>2)? 2 : drc; + if( !bQuiet ){ + if( zHistory ){ + shell_stifle_history(2000); + shell_write_history(zHistory); + free(zHistory); + } } }else{ - data.in = stdin; - rc = process_input(&data); + data.pInSource = &stdInSource; /* read from stdin without prompts */ + drc = process_input(&data); + rc = (drc>2)? 2 : drc; } } - free(azCmd); - set_table_name(&data, 0); - if( data.db ){ + shell_bail: + set_table_name(&datax, 0); + if( datax.dbUser ){ session_close_all(&data, -1); - close_db(data.db); +#if SHELL_DYNAMIC_EXTENSION + notify_subscribers(&data, NK_DbAboutToClose, datax.dbUser); +#endif + close_db(datax.dbUser); } + sqlite3_mutex_free(pGlobalDbLock); + pGlobalDbLock = 0; for(i=0; i +#include "shx_link.h" + +SQLITE_EXTENSION_INIT1; + +SHELL_EXTENSION_INIT1(pShExtApi, pExtHelpers, shextLinkFetcher); +#define SHX_API(entry) pShExtApi->entry +#define SHX_HELPER(entry) pExtHelpers->entry + +typedef struct BatBeing BatBeing; +static void sayHowMany( BatBeing *pbb, FILE *out, ShellExState *psx ); + +/* These DERIVED_METHOD(...) macro calls' arguments were copied and + * pasted from the DotCommand interface declaration in shext_linkage.h , + * but with "Interface,Derived" substituted for the interface typename. + * The function bodies are not so easily written, of course. */ + +DERIVED_METHOD(void, destruct, DotCommand,BatBeing, 0, ()){ ++ (void)(pThis); + fprintf(stdout, "BatBeing unbecoming.\n"); +} + +DERIVED_METHOD(const char *, name, DotCommand,BatBeing, 0,()){ ++ (void)(pThis); + return "bat_being"; +} + +DERIVED_METHOD(const char *, help, DotCommand,BatBeing, 1,(const char *zHK)){ ++ (void)(pThis); + if( !zHK ) + return ".bat_being ?whatever? Demonstrates vigilantism weekly\n"; + if( !*zHK ) + return " Options summon side-kick and villains.\n"; + return 0; +} + +DERIVED_METHOD(DotCmdRC, argsCheck, DotCommand,BatBeing, 3, + (char **pzErrMsg, int nArgs, char *azArgs[])){ ++ (void)(pThis); ++ (void)(pzErrMsg); ++ (void)(nArgs); ++ (void)(azArgs); + return DCR_Ok; +} + +DERIVED_METHOD(DotCmdRC, execute, DotCommand,BatBeing, 4, + (ShellExState *psx, char **pzErrMsg, int nArgs, char *azArgs[])); + +/* Define a DotCommand v-table initialized to reference above methods. */ +DotCommand_IMPLEMENT_VTABLE(BatBeing, batty_methods); + +/* Define/initialize BatBeing as a DotCommand subclass using above v-table. + * This compiles in a type-safe manner because the batty_methods v-table + * and methods it incorporates strictly match the DotCommand interface. + */ +INSTANCE_BEGIN(BatBeing); + int numCalls; + DotCommand * pPrint; + DotCommand * pPrior; +INSTANCE_END(BatBeing) batty = { + &batty_methods, + 0, 0, 0 +}; + +DERIVED_METHOD(DotCmdRC, execute, DotCommand,BatBeing, 4, + (ShellExState *psx, char **pzErrMsg, int nArgs, char *azArgs[])){ + FILE *out = pExtHelpers->currentOutputFile(psx); + BatBeing *pbb = (BatBeing*)pThis; + switch( nArgs ){ + default: + { + if( pbb->pPrior ){ + char *az1 = azArgs[1]; + for( int i=2; ipPrior->pMethods->execute(pbb->pPrior, psx, + pzErrMsg, nArgs, azArgs); + }else{ + int cix; + SHX_HELPER(setColumnWidths)(psx, azArgs+1, nArgs-1); + fprintf(out, "Column widths:"); + for( cix=0; cixnumWidths; ++cix ){ + fprintf(out, " %d", psx->pSpecWidths[cix]); + } + fprintf(out, "\n"); + } + } + break; + case 3: + fprintf(out, "The Penguin, Joker and Riddler have teamed up!\n"); + case 2: fprintf(out, "The Dynamic Duo arrives, and ... "); + case 1: fprintf(out, "@#$ KaPow! $#@\n"); + } + sayHowMany((BatBeing *)pThis, out, psx); + return DCR_Ok; +} + +static void sayHowMany( BatBeing *pbb, FILE *out, ShellExState *psx ){ + if( pbb->pPrint ){ + char *az[] = { "print", 0 }; + char *zErr = 0; + DotCommand * pdcPrint = pbb->pPrint; + DotCmdRC rc; + az[1] = sqlite3_mprintf("This execute has been called %d times.", + ++pbb->numCalls); + rc = pdcPrint->pMethods->execute(pdcPrint, psx, &zErr, 2, az); + sqlite3_free(az[1]); + if( rc!= DCR_Ok ){ + fprintf(out, "print() failed: %d\n", rc); + } + } +} + +static int shellEventHandle(void *pv, NoticeKind nk, + void *pvSubject, ShellExState *psx){ + FILE *out = pExtHelpers->currentOutputFile(psx); + if( nk==NK_ShutdownImminent ){ + BatBeing *pbb = (BatBeing *)pv; + fprintf(out, "Bat cave meteor strike detected after %d calls.\n", + pbb->numCalls); + }else if( nk==NK_Unsubscribe ){ + fprintf(out, "BatBeing incommunicado.\n"); + }else if( nk==NK_DbUserAppeared || nk==NK_DbUserVanishing ){ + const char *zWhat = (nk==NK_DbUserAppeared)? "appeared" : "vanishing"; + int isDbu = pvSubject==psx->dbUser; + fprintf(out, "db%s %s\n", isDbu? "User" : "?", zWhat); + if( psx->dbUser != pvSubject ) fprintf(out, "not dbx(%p)\n", psx->dbUser); + }else if( nk==NK_DbAboutToClose ){ + const char *zdb = (pvSubject==psx->dbUser)? "User" + : (pvSubject==psx->dbShell)? "Shell" : "?"; + fprintf(out, "db%s closing\n", zdb); + } + return 0; +} + +/* +** Extension load function. +*/ +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_testshellextc_init( + sqlite3 *db, + char **pzErrMsg, + const sqlite3_api_routines *pApi +){ + int nErr = 0; + int iLdErr; + SQLITE_EXTENSION_INIT2(pApi); + SHELL_EXTENSION_INIT2(pShExtLink, shextLinkFetcher, db); + + SHELL_EXTENSION_INIT3(pShExtApi, pExtHelpers, pShExtLink); + iLdErr = SHELL_EXTENSION_LOADFAIL_WHY(pShExtLink, 5, 5); + if( iLdErr!=EXLD_Ok ){ + *pzErrMsg = sqlite3_mprintf("Load failed, cause %d\n", iLdErr); + return SQLITE_ERROR; + }else{ + ShellExState *psx = pShExtLink->pSXS; + DotCommand *pdc = (DotCommand *)&batty; + int rc; + char *zLoadArgs = sqlite3_mprintf("Load arguments:"); + int ila; + + for( ila=0; ilanLoadArgs; ++ila ){ + zLoadArgs = sqlite3_mprintf("%z %s", zLoadArgs, + pShExtLink->azLoadArgs[ila]); + } + if( ila ) fprintf(SHX_HELPER(currentOutputFile)(psx), "%s\n", zLoadArgs); + sqlite3_free(zLoadArgs); + SHX_API(subscribeEvents)(psx, sqlite3_testshellextc_init, &batty, + NK_CountOf, shellEventHandle); + batty.pPrint = SHX_HELPER(findDotCommand)("print", psx, &rc); + batty.pPrior = SHX_HELPER(findDotCommand)("bat_being", psx, &rc); + rc = SHX_API(registerDotCommand)(psx, sqlite3_testshellextc_init, pdc); + if( rc!=0 ) ++nErr; + pShExtLink->eid = sqlite3_testshellextc_init; + } + return nErr ? SQLITE_ERROR : SQLITE_OK; +} diff --cc src/test_shellext_cpp.cpp index 6b203b0b22,0000000000..61b2a588a7 mode 100644,000000..100644 --- a/src/test_shellext_cpp.cpp +++ b/src/test_shellext_cpp.cpp @@@ -1,184 -1,0 +1,187 @@@ +/* +** 2022 Feb 28 +** +** 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 extension for testing the shell's .load -shext function. +** To build from the SQLite project root: + g++ -shared -fPIC -Wall -I. -g src/test_shellext_cpp.cpp \ + -o test_shellext_cpp.so +*/ +#include +#include "shx_link.h" + +SQLITE_EXTENSION_INIT1; + +SHELL_EXTENSION_INIT1(pShExtApi, pExtHelpers, shextLinkFetcher); +#define SHX_API(entry) pShExtApi->entry +#define SHX_HELPER(entry) pExtHelpers->entry + +struct BatBeing : DotCommand { + + ~BatBeing() { + fprintf(stdout, "BatBeing RIP.\n"); + }; // No held resources; copy/assign is fine and dying is easy. + + void destruct() { + fprintf(stdout, "BatBeing unbecoming.\n"); + } + + const char *name() { return "bat_being"; }; + + const char *help(const char *zHK) { + if( !zHK ) + return ".bat_being ?whatever? Demonstrates vigilantism weekly\n"; + if( !*zHK ) + return " Options summon side-kick and villains.\n"; + return 0; + }; + + DotCmdRC argsCheck(char **pzErrMsg, int nArgs, char *azArgs[]) { ++ (void)(pzErrMsg); ++ (void)(nArgs); ++ (void)(azArgs); + return DCR_Ok; + }; + DotCmdRC execute(ShellExState *psx, char **pzErrMsg, + int nArgs, char *azArgs[]); + + BatBeing(DotCommand *pp = 0) { + numCalls = 0; + pPrint = pp; + pPrior = 0; + fprintf(stdout, "BatBeing lives.\n"); + }; + + // Default copy/assign are fine; nothing held. + + int numCalls; + DotCommand * pPrint; + DotCommand * pPrior; +}; + +static void sayHowMany( BatBeing *pbb, FILE *out, ShellExState *psx ){ + if( pbb->pPrint ){ + static char cmd[] = "print"; + char *az[] = { cmd, 0 }; + char *zErr = 0; + DotCmdRC rc; + az[1] = sqlite3_mprintf("This execute has been called %d times.", + ++pbb->numCalls); + rc = pbb->pPrint->execute(psx, &zErr, 2, az); + sqlite3_free(az[1]); + if( rc!= DCR_Ok ){ + fprintf(out, "print() failed: %d\n", rc); + } + } +} + +DotCmdRC BatBeing::execute(ShellExState *psx, char **pzErrMsg, + int nArgs, char *azArgs[]) { + FILE *out = SHX_HELPER(currentOutputFile)(psx); + switch( nArgs ){ + default: + { + if( pPrior ){ + char *az1 = azArgs[1]; + for( int i=2; iexecute(psx, pzErrMsg, nArgs, azArgs); + }else{ + SHX_HELPER(setColumnWidths)(psx, azArgs+1, nArgs-1); + fprintf(out, "Column widths:"); + for( int cix=0; cixnumWidths; ++cix ){ + fprintf(out, " %d", psx->pSpecWidths[cix]); + } + fprintf(out, "\n"); + } + } + break; + case 3: + fprintf(out, "The Penguin, Joker and Riddler have teamed up!\n"); + case 2: fprintf(out, "The Dynamic Duo arrives, and ... "); + case 1: fprintf(out, "@#$ KaPow! $#@\n"); + } + sayHowMany(this, out, psx); + return DCR_Ok; +} + +/* Define/initialize BatBeing as a DotCommand subclass using above v-table. + * This compiles in a type-safe manner because the batty_methods v-table + * and methods it incorporates strictly match the DotCommand interface. + */ +static BatBeing batty(0); + +static int shellEventHandle(void *pv, NoticeKind nk, + void *pvSubject, ShellExState *psx){ + FILE *out = SHX_HELPER(currentOutputFile)(psx); + if( nk==NK_ShutdownImminent ){ + BatBeing *pbb = (BatBeing *)pv; + fprintf(out, "Bat cave meteor strike detected after %d calls.\n", + pbb->numCalls); + }else if( nk==NK_Unsubscribe ){ + fprintf(out, "BatBeing incommunicado.\n"); + }else if( nk==NK_DbUserAppeared || nk==NK_DbUserVanishing ){ + const char *zWhat = (nk==NK_DbUserAppeared)? "appeared" : "vanishing"; + int isDbu = pvSubject==psx->dbUser; + fprintf(out, "db%s %s\n", isDbu? "User" : "?", zWhat); + if( psx->dbUser != pvSubject ) fprintf(out, "not dbx(%p)\n", psx->dbUser); + }else if( nk==NK_DbAboutToClose ){ + const char *zdb = (pvSubject==psx->dbUser)? "User" + : (pvSubject==psx->dbShell)? "Shell" : "?"; + fprintf(out, "db%s closing\n", zdb); + } + return 0; +} + +/* +** Extension load function. +*/ +extern "C" +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_testshellextcpp_init( + sqlite3 *db, + char **pzErrMsg, + const sqlite3_api_routines *pApi +){ + int iLdErr; + int nErr = 0; + SQLITE_EXTENSION_INIT2(pApi); + SHELL_EXTENSION_INIT2(pShExtLink, shextLinkFetcher, db); + + SHELL_EXTENSION_INIT3(pShExtApi, pExtHelpers, pShExtLink); + iLdErr = SHELL_EXTENSION_LOADFAIL_WHY(pShExtLink, 5, 5); + if( iLdErr!=EXLD_Ok ){ + *pzErrMsg = sqlite3_mprintf("Load failed, cause %d\n", iLdErr); + return SQLITE_ERROR; + }else{ + ShellExState *psx = pShExtLink->pSXS; + int rc; + char *zLoadArgs = sqlite3_mprintf("Load arguments:"); + int ila; + + for( ila=0; ilanLoadArgs; ++ila ){ + zLoadArgs = sqlite3_mprintf("%z %s", zLoadArgs, + pShExtLink->azLoadArgs[ila]); + } + if( ila ) fprintf(SHX_HELPER(currentOutputFile)(psx), "%s\n", zLoadArgs); + sqlite3_free(zLoadArgs); + SHX_API(subscribeEvents)(psx, sqlite3_testshellextcpp_init, &batty, + NK_CountOf, shellEventHandle); + batty.pPrint = SHX_HELPER(findDotCommand)("print", psx, &rc); + batty.pPrior = SHX_HELPER(findDotCommand)(batty.name(), psx, &rc); + rc = SHX_API(registerDotCommand)(psx, + sqlite3_testshellextcpp_init, &batty); + if( rc!=0 ) ++nErr; + pShExtLink->eid = sqlite3_testshellextcpp_init; + } + return nErr ? SQLITE_ERROR : SQLITE_OK; +}