From: larrybr Date: Sat, 9 Apr 2022 19:39:58 +0000 (+0000) Subject: Sync w/trunk, for .import fix. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=866c30285de333428facfb8ac2a7e53d667ffd85;p=thirdparty%2Fsqlite.git Sync w/trunk, for .import fix. FossilOrigin-Name: 861ab023be283782131df5ccd9eff50cf2f06f48f1ea7defd3f08177a3df5889 --- 866c30285de333428facfb8ac2a7e53d667ffd85 diff --cc ext/misc/tclshext.c.in index 5374c66701,0000000000..b27c698cf9 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) +# 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); + /* 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, MetaCommand,TclCmd, 0, ()){ + /* Nothing to do, instance data is static. */ + (void)(pThis); +} +static DERIVED_METHOD(void, destruct, MetaCommand,UnkCmd, 0, ()); + +DERIVED_METHOD(void, destruct, ScriptSupport,TclSS, 0, ()){ + /* Nothing to do, instance data is static. */ + (void)(pThis); +} + +DERIVED_METHOD(const char *, name, MetaCommand,TclCmd, 0,()){ + return "tcl"; +} +DERIVED_METHOD(const char *, name, MetaCommand,UnkCmd, 0,()){ + return "unknown"; +} + +DERIVED_METHOD(const char *, help, MetaCommand,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, MetaCommand,UnkCmd, 1,(const char *zHK)); + +DERIVED_METHOD(DotCmdRC, argsCheck, MetaCommand,TclCmd, 3, + (char **pzErrMsg, int nArgs, char *azArgs[])){ + return DCR_Ok; +} +DERIVED_METHOD(DotCmdRC, argsCheck, MetaCommand,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 != "" && $interactive} { ++ if {$result != "" && $::tcl_interactive} { + puts $result + } + } elseif {$rc == 1} { + puts stderr "Error: $result" + } elseif {$rc == 2} { + return -code 2 + } + } - if {$interactive && $trimmed ne "."} {puts {}} ++ 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, MetaCommand,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, MetaCommand,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_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)); + } + 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; + MetaCommand *pmc = 0; + int nFound = 0; + int ia, rc; + + if( name ) pmc = SHX_HELPER(findMetaCommand)(name, psx, &nFound); + if( pmc==(MetaCommand*)&tclcmd && nArgs==2 ){ + /* Will not do a nested REPL, just silently semi-fake it. */ + return TCL_OK; + } + if( pmc && nFound==1 ){ + /* Run the dot command and interpret its returns. */ + DotCmdRC drc = SHX_HELPER(runMetaCommand)(pmc, (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(registerMetaCommand)(psx, sqlite3_tclshext_init, + (MetaCommand *)&unkcmd); + rc = SHX_API(registerMetaCommand)(psx, sqlite3_tclshext_init, + (MetaCommand *)&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 84a243c4b5,e767080f57..50183c67da --- a/manifest +++ b/manifest @@@ -1,5 -1,5 +1,5 @@@ - C Get\stclshext\smade\swith\sTk,\soptionally,\sand\smake\sunknown\swork\sas\sin\stclsh. - D 2022-04-09T14:57:17.905 -C Fix\s.import\sbug\sreported\sat\shttps://sqlite.org/forum/forumpost/14db09d7e765b819\s.\szAutoColumn\smade\sto\sdeliver\scharacters,\snot\sbytes. -D 2022-04-09T18:51:49.784 ++C Sync\sw/trunk,\sfor\s.import\sfix. ++D 2022-04-09T19:39:58.635 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@@ -329,7 -328,6 +329,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 026416fd5c1e84eb8c8e98edc9dcd826e3c0d32fd0883ab306829642867128e8 ++F ext/misc/tclshext.c.in c29c3aa45cc477106aa6995308c8e1f8fe68e6ff4b10b2d906acefbbd62c4196 F ext/misc/templatevtab.c 8a16a91a5ceaccfcbd6aaaa56d46828806e460dd194965b3f77bf38f14b942c4 F ext/misc/totype.c fa4aedeb07f66169005dffa8de3b0a2b621779fd44f85c103228a42afa71853b F ext/misc/uint.c 053fed3bce2e89583afcd4bf804d75d659879bbcedac74d0fa9ed548839a030b @@@ -555,9 -552,8 +555,9 @@@ F src/printf.c 05d8dfd2018bc4fc3ddb8b37 F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c F src/resolve.c 18d99e7146852d6064559561769fcca0743eb32b14a97da6dbed373a30ee0e76 F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 - F src/select.c d6c04eb93395024af80f61a8c278a33c2a0333aeb7d57bb6aa737a6f1c4af4b8 - F src/shell.c.in 8e2ad2912ae381786653ce9db04522f8058873d690f434016ecda5a419c0cdc5 + F src/select.c 7c106b3f36d483242b0a9c696614cd53d6f29e1ac81da6a3f0e9ea92f4211cc3 -F src/shell.c.in eb7f10d5e2c47bd014d92ec5db1def21fcc1ed56ffaaa4ee715b6c37c370b47f ++F src/shell.c.in 3984e1de9e70357ea0a017409f4421eacf94175a30969d68f6cc135a4c1fb9f9 +F src/shext_linkage.h 88a3f215fdb090fcc3b3577a05bafd234f1a556bad3f2f4ac990a177aebf4b2c F src/sqlite.h.in 2a35f62185eb5e7ecc64a2f68442b538ce9be74f80f28a00abc24837edcf1c17 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h f49e28c25bd941e79794db5415fdf7b202deb3bc072ed6f1ed273d578703684e @@@ -1394,11 -1389,11 +1394,11 @@@ F test/sharedA.test 49d87ec54ab640fbbc3 F test/sharedB.test 16cc7178e20965d75278f410943109b77b2e645e F test/shared_err.test 32634e404a3317eeb94abc7a099c556a346fdb8fb3858dbe222a4cbb8926a939 F test/sharedlock.test 5ede3c37439067c43b0198f580fd374ebf15d304 -F test/shell1.test b224e0793c5f48aa3749e65d8c64b93a30731bd206f2e41e6c5f1bee1bdb16c6 -F test/shell2.test 7a3a23a9f57b99453f1679b1fe8072cb30e382a622874c0c4d97695fadb0a787 -F test/shell3.test a50628ab1d78d90889d9d3f32fb2c084ee15674771e96afe954aaa0accd1de3c -F test/shell4.test 8f6c0fce4abed19a8a7f7262517149812a04caa905d01bdc8f5e92573504b759 -F test/shell5.test 78a7a8516b1e7de560748881424f621321549023d3e5f7ed2e1c56497f64c06c +F test/shell1.test 9401659c4f7319586ddd36aac15aaadff0092caa786589fc20ad069fc2cb1c74 +F test/shell2.test fc6bb55f5ceaaffa284cb994aa00fd56f7ead09949c9db01c3846d65a76a7748 +F test/shell3.test 4ddea2bd182e7e03249911b23ae249e7cb8a91cdc86e695198725affabe8ecd3 +F test/shell4.test b232688061cce531f42ec067f3b5760e31d12409e566e2ae230951036dd156f1 - F test/shell5.test 04b46462b3297de7195aaed99a0cd99fdb9f8de211a96abea46a81db55c5ef77 ++F test/shell5.test 39d2cffb3c1c67456b2c42eb5e46bec1e3780c68a6d71bb156e012d3f53733c5 F test/shell6.test 1ceb51b2678c472ba6cf1e5da96679ce8347889fe2c3bf93a0e0fa73f00b00d3 F test/shell7.test 115132f66d0463417f408562cc2cf534f6bbc6d83a6d50f0072a9eb171bae97f F test/shell8.test 388471d16e4de767333107e30653983f186232c0e863f4490bb230419e830aae @@@ -1951,8 -1945,8 +1951,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 0b3bb0e793f8c7fd79042322b7748a75d17a3c3d0752cfb0f2c544af49b1d6d6 - R fd77dc9b9d6010cf74b598e30b2c6a7a -P f237e1d8cc41b937f34288daebfacf5f7b0990a807a805e0cb6b45bc730192d6 -R 32d747db1a8abc3115c9e0c3c720312e ++P 43eb311e517b79cde9e17c1a80baed8d13d9d943dd9ee44b31831159df8715fc 21e96600d90c1cda84777abe22a11058eba46c9faefeb05f8c31bc0e7fa84b19 ++R 67ea5330fa92b3979fbd7e883fdffd1f U larrybr - Z 41d0aa7cc99a4d24d4e075af36bc0e99 -Z 31d16ad9966991b7487e6e03ff8edde4 ++Z b47b9cb62392429660769c050bed79ba # Remove this line to create a well-formed Fossil manifest. diff --cc manifest.uuid index 3448532c48,b96920f440..6826a57cdb --- a/manifest.uuid +++ b/manifest.uuid @@@ -1,1 -1,1 +1,1 @@@ - 43eb311e517b79cde9e17c1a80baed8d13d9d943dd9ee44b31831159df8715fc -21e96600d90c1cda84777abe22a11058eba46c9faefeb05f8c31bc0e7fa84b19 ++861ab023be283782131df5ccd9eff50cf2f06f48f1ea7defd3f08177a3df5889 diff --cc src/shell.c.in index e8a3046395,996a7f942b..74c6ed74e0 --- a/src/shell.c.in +++ b/src/shell.c.in @@@ -7404,715 -6820,355 +7404,715 @@@ static void recoverFreeTable(RecoverTab } /* -** Implementation of .ar "eXtract" command. -*/ -static int arExtractCommand(ArCommand *pAr){ - const char *zSql1 = - "SELECT " - " ($dir || name)," - " writefile(($dir || name), %s, mode, mtime) " - "FROM %s WHERE (%s) AND (data IS NULL OR $dirOnly = 0)" - " AND name NOT GLOB '*..[/\\]*'"; +** This function is a no-op if (*pRc) is not SQLITE_OK when it is called. +** Otherwise, it allocates and returns a RecoverTable object based on the +** final four arguments passed to this function. It is the responsibility +** of the caller to eventually free the returned object using +** recoverFreeTable(). +*/ +static RecoverTable *recoverNewTable( + int *pRc, /* IN/OUT: Error code */ + const char *zName, /* Name of table */ + const char *zSql, /* CREATE TABLE statement */ + int bIntkey, + int nCol +){ + sqlite3 *dbtmp = 0; /* sqlite3 handle for testing CREATE TABLE */ + int rc = *pRc; + RecoverTable *pTab = 0; - const char *azExtraArg[] = { - "sqlar_uncompress(data, sz)", - "data" - }; + pTab = (RecoverTable*)shellMalloc(&rc, sizeof(RecoverTable)); + if( rc==SQLITE_OK ){ + int nSqlCol = 0; + int bSqlIntkey = 0; + sqlite3_stmt *pStmt = 0; - sqlite3_stmt *pSql = 0; - int rc = SQLITE_OK; - char *zDir = 0; - char *zWhere = 0; - int i, j; + rc = sqlite3_open("", &dbtmp); + if( rc==SQLITE_OK ){ + sqlite3_create_function(dbtmp, "shell_idquote", 1, SQLITE_UTF8, 0, + shellIdQuote, 0, 0); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(dbtmp, "PRAGMA writable_schema = on", 0, 0, 0); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(dbtmp, zSql, 0, 0, 0); + if( rc==SQLITE_ERROR ){ + rc = SQLITE_OK; + goto finished; + } + } + shellPreparePrintf(dbtmp, &rc, &pStmt, + "SELECT count(*) FROM pragma_table_info(%Q)", zName + ); + if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + nSqlCol = sqlite3_column_int(pStmt, 0); + } + shellFinalize(&rc, pStmt); - /* If arguments are specified, check that they actually exist within - ** the archive before proceeding. And formulate a WHERE clause to - ** match them. */ - rc = arCheckEntries(pAr); - arWhereClause(&rc, pAr, &zWhere); + if( rc!=SQLITE_OK || nSqlColzDir ){ - zDir = sqlite3_mprintf("%s/", pAr->zDir); - }else{ - zDir = sqlite3_mprintf(""); + shellPreparePrintf(dbtmp, &rc, &pStmt, + "SELECT (" + " SELECT substr(data,1,1)==X'0D' FROM sqlite_dbpage WHERE pgno=rootpage" + ") FROM sqlite_schema WHERE name = %Q", zName + ); + if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + bSqlIntkey = sqlite3_column_int(pStmt, 0); } - if( zDir==0 ) rc = SQLITE_NOMEM; - } + shellFinalize(&rc, pStmt); - shellPreparePrintf(pAr->db, &rc, &pSql, zSql1, - azExtraArg[pAr->bZip], pAr->zSrcTable, zWhere - ); + if( bIntkey==bSqlIntkey ){ + int i; + const char *zPk = "_rowid_"; + sqlite3_stmt *pPkFinder = 0; - if( rc==SQLITE_OK ){ - j = sqlite3_bind_parameter_index(pSql, "$dir"); - sqlite3_bind_text(pSql, j, zDir, -1, SQLITE_STATIC); + /* If this is an intkey table and there is an INTEGER PRIMARY KEY, + ** set zPk to the name of the PK column, and pTab->iPk to the index + ** of the column, where columns are 0-numbered from left to right. + ** Or, if this is a WITHOUT ROWID table or if there is no IPK column, + ** leave zPk as "_rowid_" and pTab->iPk at -2. */ + pTab->iPk = -2; + if( bIntkey ){ + shellPreparePrintf(dbtmp, &rc, &pPkFinder, + "SELECT cid, name FROM pragma_table_info(%Q) " + " WHERE pk=1 AND type='integer' COLLATE nocase" + " AND NOT EXISTS (SELECT cid FROM pragma_table_info(%Q) WHERE pk=2)" + , zName, zName + ); + if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPkFinder) ){ + pTab->iPk = sqlite3_column_int(pPkFinder, 0); + zPk = (const char*)sqlite3_column_text(pPkFinder, 1); + if( zPk==0 ){ zPk = "_"; /* Defensive. Should never happen */ } + } + } - /* Run the SELECT statement twice. The first time, writefile() is called - ** for all archive members that should be extracted. The second time, - ** only for the directories. This is because the timestamps for - ** extracted directories must be reset after they are populated (as - ** populating them changes the timestamp). */ - for(i=0; i<2; i++){ - j = sqlite3_bind_parameter_index(pSql, "$dirOnly"); - sqlite3_bind_int(pSql, j, i); - if( pAr->bDryRun ){ - utf8_printf(pAr->p->out, "%s\n", sqlite3_sql(pSql)); + pTab->zQuoted = shellMPrintf(&rc, "\"%w\"", zName); + pTab->azlCol = (char**)shellMalloc(&rc, sizeof(char*) * (nSqlCol+1)); + pTab->nCol = nSqlCol; + + if( bIntkey ){ + pTab->azlCol[0] = shellMPrintf(&rc, "\"%w\"", zPk); }else{ - while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pSql) ){ - if( i==0 && pAr->bVerbose ){ - utf8_printf(pAr->p->out, "%s\n", sqlite3_column_text(pSql, 0)); - } - } + pTab->azlCol[0] = shellMPrintf(&rc, ""); } - shellReset(&rc, pSql); + i = 1; + shellPreparePrintf(dbtmp, &rc, &pStmt, + "SELECT %Q || group_concat(shell_idquote(name), ', ') " + " FILTER (WHERE cid!=%d) OVER (ORDER BY %s cid) " + "FROM pragma_table_info(%Q)", + bIntkey ? ", " : "", pTab->iPk, + bIntkey ? "" : "(CASE WHEN pk=0 THEN 1000000 ELSE pk END), ", + zName + ); + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *zText = (const char*)sqlite3_column_text(pStmt, 0); + pTab->azlCol[i] = shellMPrintf(&rc, "%s%s", pTab->azlCol[0], zText); + i++; + } + shellFinalize(&rc, pStmt); + + shellFinalize(&rc, pPkFinder); } - shellFinalize(&rc, pSql); } - sqlite3_free(zDir); - sqlite3_free(zWhere); - return rc; -} - -/* -** Run the SQL statement in zSql. Or if doing a --dryrun, merely print it out. -*/ -static int arExecSql(ArCommand *pAr, const char *zSql){ - int rc; - if( pAr->bDryRun ){ - utf8_printf(pAr->p->out, "%s\n", zSql); - rc = SQLITE_OK; - }else{ - char *zErr = 0; - rc = sqlite3_exec(pAr->db, zSql, 0, 0, &zErr); - if( zErr ){ - utf8_printf(stdout, "ERROR: %s\n", zErr); - sqlite3_free(zErr); - } + finished: + sqlite3_close(dbtmp); + *pRc = rc; + if( rc!=SQLITE_OK || (pTab && pTab->zQuoted==0) ){ + recoverFreeTable(pTab); + pTab = 0; } - return rc; + return pTab; } - /* -** Implementation of .ar "create", "insert", and "update" commands. -** -** create -> Create a new SQL archive -** insert -> Insert or reinsert all files listed -** update -> Insert files that have changed or that were not -** previously in the archive -** -** Create the "sqlar" table in the database if it does not already exist. -** Then add each file in the azFile[] array to the archive. Directories -** are added recursively. If argument bVerbose is non-zero, a message is -** printed on stdout for each file archived. +** This function is called to search the schema recovered from the +** sqlite_schema table of the (possibly) corrupt database as part +** of a ".recover" command. Specifically, for a table with root page +** iRoot and at least nCol columns. Additionally, if bIntkey is 0, the +** table must be a WITHOUT ROWID table, or if non-zero, not one of +** those. ** -** The create command is the same as update, except that it drops -** any existing "sqlar" table before beginning. The "insert" command -** always overwrites every file named on the command-line, where as -** "update" only overwrites if the size or mtime or mode has changed. +** If a table is found, a (RecoverTable*) object is returned. Or, if +** no such table is found, but bIntkey is false and iRoot is the +** root page of an index in the recovered schema, then (*pbNoop) is +** set to true and NULL returned. Or, if there is no such table or +** index, NULL is returned and (*pbNoop) set to 0, indicating that +** the caller should write data to the orphans table. */ -static int arCreateOrUpdateCommand( - ArCommand *pAr, /* Command arguments and options */ - int bUpdate, /* true for a --create. */ - int bOnlyIfChanged /* Only update if file has changed */ +static RecoverTable *recoverFindTable( + sqlite3 *db, /* DB from which to recover */ + int *pRc, /* IN/OUT: Error code */ + int iRoot, /* Root page of table */ + int bIntkey, /* True for an intkey table */ + int nCol, /* Number of columns in table */ + int *pbNoop /* OUT: True if iRoot is root of index */ ){ - const char *zCreate = - "CREATE TABLE IF NOT EXISTS sqlar(\n" - " name TEXT PRIMARY KEY, -- name of the file\n" - " mode INT, -- access permissions\n" - " mtime INT, -- last modification time\n" - " sz INT, -- original file size\n" - " data BLOB -- compressed content\n" - ")"; - const char *zDrop = "DROP TABLE IF EXISTS sqlar"; - const char *zInsertFmt[2] = { - "REPLACE INTO %s(name,mode,mtime,sz,data)\n" - " SELECT\n" - " %s,\n" - " mode,\n" - " mtime,\n" - " CASE substr(lsmode(mode),1,1)\n" - " WHEN '-' THEN length(data)\n" - " WHEN 'd' THEN 0\n" - " ELSE -1 END,\n" - " sqlar_compress(data)\n" - " FROM fsdir(%Q,%Q) AS disk\n" - " WHERE lsmode(mode) NOT LIKE '?%%'%s;" - , - "REPLACE INTO %s(name,mode,mtime,data)\n" - " SELECT\n" - " %s,\n" - " mode,\n" - " mtime,\n" - " data\n" - " FROM fsdir(%Q,%Q) AS disk\n" - " WHERE lsmode(mode) NOT LIKE '?%%'%s;" - }; - int i; /* For iterating through azFile[] */ - int rc; /* Return code */ - const char *zTab = 0; /* SQL table into which to insert */ - char *zSql; - char zTemp[50]; - char *zExists = 0; + sqlite3_stmt *pStmt = 0; + RecoverTable *pRet = 0; + int bNoop = 0; + const char *zSql = 0; + const char *zName = 0; + + /* Search the recovered schema for an object with root page iRoot. */ + shellPreparePrintf(db, pRc, &pStmt, + "SELECT type, name, sql FROM recovery.schema WHERE rootpage=%d", iRoot + ); + while( *pRc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *zType = (const char*)sqlite3_column_text(pStmt, 0); + if( bIntkey==0 && sqlite3_stricmp(zType, "index")==0 ){ + bNoop = 1; + break; + } + if( sqlite3_stricmp(zType, "table")==0 ){ + zName = (const char*)sqlite3_column_text(pStmt, 1); + zSql = (const char*)sqlite3_column_text(pStmt, 2); + if( zName!=0 && zSql!=0 ){ + pRet = recoverNewTable(pRc, zName, zSql, bIntkey, nCol); + break; + } + } + } + + shellFinalize(pRc, pStmt); + *pbNoop = bNoop; + return pRet; +} - arExecSql(pAr, "PRAGMA page_size=512"); - rc = arExecSql(pAr, "SAVEPOINT ar;"); - if( rc!=SQLITE_OK ) return rc; - zTemp[0] = 0; - if( pAr->bZip ){ - /* Initialize the zipfile virtual table, if necessary */ - if( pAr->zFile ){ - sqlite3_uint64 r; - sqlite3_randomness(sizeof(r),&r); - sqlite3_snprintf(sizeof(zTemp),zTemp,"zip%016llx",r); - zTab = zTemp; - zSql = sqlite3_mprintf( - "CREATE VIRTUAL TABLE temp.%s USING zipfile(%Q)", - zTab, pAr->zFile - ); - rc = arExecSql(pAr, zSql); - sqlite3_free(zSql); - }else{ - zTab = "zip"; +/* +** Return a RecoverTable object representing the orphans table. +*/ +static RecoverTable *recoverOrphanTable( + sqlite3 *db, /* DB from which to recover */ + FILE *out, /* Where to put recovery DDL */ + int *pRc, /* IN/OUT: Error code */ + const char *zLostAndFound, /* Base name for orphans table */ + int nCol /* Number of user data columns */ +){ + RecoverTable *pTab = 0; + if( nCol>=0 && *pRc==SQLITE_OK ){ + int i; + + /* This block determines the name of the orphan table. The prefered + ** name is zLostAndFound. But if that clashes with another name + ** in the recovered schema, try zLostAndFound_0, zLostAndFound_1 + ** and so on until a non-clashing name is found. */ + int iTab = 0; + char *zTab = shellMPrintf(pRc, "%s", zLostAndFound); + sqlite3_stmt *pTest = 0; + shellPrepare(db, pRc, + "SELECT 1 FROM recovery.schema WHERE name=?", &pTest + ); + if( pTest ) sqlite3_bind_text(pTest, 1, zTab, -1, SQLITE_TRANSIENT); + while( *pRc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pTest) ){ + shellReset(pRc, pTest); + sqlite3_free(zTab); + zTab = shellMPrintf(pRc, "%s_%d", zLostAndFound, iTab++); + sqlite3_bind_text(pTest, 1, zTab, -1, SQLITE_TRANSIENT); } - }else{ - /* Initialize the table for an SQLAR */ - zTab = "sqlar"; - if( bUpdate==0 ){ - rc = arExecSql(pAr, zDrop); - if( rc!=SQLITE_OK ) goto end_ar_transaction; + shellFinalize(pRc, pTest); + + pTab = (RecoverTable*)shellMalloc(pRc, sizeof(RecoverTable)); + if( pTab ){ + pTab->zQuoted = shellMPrintf(pRc, "\"%w\"", zTab); + pTab->nCol = nCol; + pTab->iPk = -2; + if( nCol>0 ){ + pTab->azlCol = (char**)shellMalloc(pRc, sizeof(char*) * (nCol+1)); + if( pTab->azlCol ){ + pTab->azlCol[nCol] = shellMPrintf(pRc, ""); + for(i=nCol-1; i>=0; i--){ + pTab->azlCol[i] = shellMPrintf(pRc, "%s, NULL", pTab->azlCol[i+1]); + } + } + } + + if( *pRc!=SQLITE_OK ){ + recoverFreeTable(pTab); + pTab = 0; + }else{ + raw_printf(out, + "CREATE TABLE %s(rootpgno INTEGER, " + "pgno INTEGER, nfield INTEGER, id INTEGER", pTab->zQuoted + ); + for(i=0; ibSafeMode ) return DCR_AbortError; + for(j=1; jnArg && rc==SQLITE_OK; i++){ - char *zSql2 = sqlite3_mprintf(zInsertFmt[pAr->bZip], zTab, - pAr->bVerbose ? "shell_putsnl(name)" : "name", - pAr->azArg[i], pAr->zDir, zExists); - rc = arExecSql(pAr, zSql2); - sqlite3_free(zSql2); + if( zDestFile==0 ){ + return DCR_Missing; } -end_ar_transaction: + if( zDb==0 ) zDb = "main"; + rc = sqlite3_open_v2(zDestFile, &pDest, + SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, zVfs); if( rc!=SQLITE_OK ){ - sqlite3_exec(pAr->db, "ROLLBACK TO ar; RELEASE ar;", 0, 0, 0); + *pzErr = smprintf("cannot open \"%s\"\n", zDestFile); + close_db(pDest); + return DCR_Error; + } + if( bAsync ){ + sqlite3_exec(pDest, "PRAGMA synchronous=OFF; PRAGMA journal_mode=OFF;", + 0, 0, 0); + } + open_db(psx, 0); + pBackup = sqlite3_backup_init(pDest, "main", DBX(psx), zDb); + if( pBackup==0 ){ + *pzErr = smprintf("%s failed, %s\n", azArg[0], sqlite3_errmsg(pDest)); + close_db(pDest); + return DCR_Error; + } + while( (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK ){} + sqlite3_backup_finish(pBackup); + if( rc==SQLITE_DONE ){ + rc = 0; + }else{ + *pzErr = smprintf("%s\n", sqlite3_errmsg(pDest)); + rc = 1; + } + close_db(pDest); + return DCR_Ok|rc; +} + +/* + * zAutoColumn(zCol, &db, ?) => 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 \ ++ 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))," ++ " 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{ - rc = arExecSql(pAr, "RELEASE ar;"); - if( pAr->bZip && pAr->zFile ){ - zSql = sqlite3_mprintf("DROP TABLE %s", zTemp); - arExecSql(pAr, zSql); - sqlite3_free(zSql); + /* Formulate the columns spec, close the DB, zero *pDb. */ + char *zColsSpec = 0; + int hasDupes = db_int(*pDb, zHasDupes); +#ifdef SQLITE_ENABLE_MATH_FUNCTIONS + int nDigits = (hasDupes)? db_int(*pDb, zColDigits) : 0; +#else +# define nDigits 2 +#endif + 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); } - } - sqlite3_free(zExists); - return rc; -} - -/* -** Implementation of ".ar" dot command. -*/ -static int arDotCommand( - ShellState *pState, /* Current shell tool state */ - int fromCmdLine, /* True if -A command-line option, not .ar cmd */ - char **azArg, /* Array of arguments passed to dot command */ - int nArg /* Number of entries in azArg[] */ -){ - ArCommand cmd; - int rc; - memset(&cmd, 0, sizeof(cmd)); - cmd.fromCmdLine = fromCmdLine; - rc = arParseCommand(azArg, nArg, &cmd); - if( rc==SQLITE_OK ){ - int eDbType = SHELL_OPEN_UNSPEC; - cmd.p = pState; - cmd.db = pState->db; - if( cmd.zFile ){ - eDbType = deduceDatabaseType(cmd.zFile, 1); + /* 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{ - eDbType = pState->openMode; - } - if( eDbType==SHELL_OPEN_ZIPFILE ){ - if( cmd.eCmd==AR_CMD_EXTRACT || cmd.eCmd==AR_CMD_LIST ){ - if( cmd.zFile==0 ){ - cmd.zSrcTable = sqlite3_mprintf("zip"); - }else{ - cmd.zSrcTable = sqlite3_mprintf("zipfile(%Q)", cmd.zFile); - } - } - cmd.bZip = 1; - }else if( cmd.zFile ){ - int flags; - if( cmd.bAppend ) eDbType = SHELL_OPEN_APPENDVFS; - if( cmd.eCmd==AR_CMD_CREATE || cmd.eCmd==AR_CMD_INSERT - || cmd.eCmd==AR_CMD_REMOVE || cmd.eCmd==AR_CMD_UPDATE ){ - flags = SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE; - }else{ - flags = SQLITE_OPEN_READONLY; - } - cmd.db = 0; - if( cmd.bDryRun ){ - utf8_printf(pState->out, "-- open database '%s'%s\n", cmd.zFile, - eDbType==SHELL_OPEN_APPENDVFS ? " using 'apndvfs'" : ""); - } - rc = sqlite3_open_v2(cmd.zFile, &cmd.db, flags, - eDbType==SHELL_OPEN_APPENDVFS ? "apndvfs" : 0); - if( rc!=SQLITE_OK ){ - utf8_printf(stderr, "cannot open file: %s (%s)\n", - cmd.zFile, sqlite3_errmsg(cmd.db) - ); - goto end_ar_command; - } - sqlite3_fileio_init(cmd.db, 0, 0); - sqlite3_sqlar_init(cmd.db, 0, 0); - sqlite3_create_function(cmd.db, "shell_putsnl", 1, SQLITE_UTF8, cmd.p, - shellPutsFunc, 0, 0); - + zColsSpec = 0; } - if( cmd.zSrcTable==0 && cmd.bZip==0 && cmd.eCmd!=AR_CMD_HELP ){ - if( cmd.eCmd!=AR_CMD_CREATE - && sqlite3_table_column_metadata(cmd.db,0,"sqlar","name",0,0,0,0,0) - ){ - utf8_printf(stderr, "database does not contain an 'sqlar' table\n"); - rc = SQLITE_ERROR; - goto end_ar_command; + 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; } - cmd.zSrcTable = sqlite3_mprintf("sqlar"); } + sqlite3_finalize(pStmt); + sqlite3_close(*pDb); + *pDb = 0; + return zColsSpec; + } +} - switch( cmd.eCmd ){ - case AR_CMD_CREATE: - rc = arCreateOrUpdateCommand(&cmd, 0, 0); - break; +static FILE *currentOutputFile(ShellExState *p){ + return ISS(p)->out; +} - case AR_CMD_EXTRACT: - rc = arExtractCommand(&cmd); - break; +#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 meta-command, to be called during extension load/init. */ +static int register_meta_command(ShellExState *p, + ExtensionId eid, MetaCommand *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->numMetaCommands; + 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->ppMetaCommands + = sqlite3_realloc(psei->ppMetaCommands, (nc+1)*sizeof(MetaCommand *)); + shell_check_oom(psei->ppMetaCommands); + 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->ppMetaCommands[nc++] = pMC; + psei->numMetaCommands = nc; + notify_subscribers(psi, NK_NewDotCommand, pMC); + if( strcmp("unknown", zName)==0 ){ + psi->pUnknown = pMC; + psei->pUnknown = pMC; + } + return SQLITE_OK; + }else{ + psei->ppMetaCommands[nc] = 0; + } + } + return SQLITE_ERROR; +} - case AR_CMD_LIST: - rc = arListCommand(&cmd); - break; +/* Register an output data display (or other disposition) mode */ +static int register_exporter(ShellExState *p, + ExtensionId eid, ExportHandler *pEH){ + return SQLITE_ERROR; +} - case AR_CMD_HELP: - arUsage(pState->out); - break; +/* Register an import variation from (various sources) for .import */ +static int register_importer(ShellExState *p, + ExtensionId eid, ImportHandler *pIH){ + return SQLITE_ERROR; +} - case AR_CMD_INSERT: - rc = arCreateOrUpdateCommand(&cmd, 1, 0); - break; +/* 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; +} - case AR_CMD_REMOVE: - rc = arRemoveCommand(&cmd); - break; +/* 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; - default: - assert( cmd.eCmd==AR_CMD_UPDATE ); - rc = arCreateOrUpdateCommand(&cmd, 1, 1); - break; - } + assert(psi->pShxLoaded!=0 && p->dbShell!=0); + for( ie=psi->numExtLoaded-1; ie>0; --ie ){ + if( psi->pShxLoaded[ie].extId==eid ) break; } -end_ar_command: - if( cmd.db!=pState->db ){ - close_db(cmd.db); + 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); + const char *zLE = (nc>0 && zHelp[nc-1]!='\n')? "\n" : ""; + sqlite3_bind_text(pStmt, 3, zHelp, -1, 0); + sqlite3_bind_text(pStmt, 4, zLE, -1, 0); } - sqlite3_free(cmd.zSrcTable); - - return rc; + rc = sqlite3_step(pStmt); + sqlite3_finalize(pStmt); + return (rc==SQLITE_DONE)? SQLITE_OK : SQLITE_ERROR; } -/* End of the ".archive" or ".ar" command logic -*******************************************************************************/ -#endif /* !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB) */ -#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) /* -** If (*pRc) is not SQLITE_OK when this function is called, it is a no-op. -** Otherwise, the SQL statement or statements in zSql are executed using -** database connection db and the error code written to *pRc before -** this function returns. -*/ -static void shellExec(sqlite3 *db, int *pRc, const char *zSql){ - int rc = *pRc; - if( rc==SQLITE_OK ){ - char *zErr = 0; - rc = sqlite3_exec(db, zSql, 0, 0, &zErr); - if( rc!=SQLITE_OK ){ - raw_printf(stderr, "SQL error: %s\n", zErr); + * 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; + } } - sqlite3_free(zErr); - *pRc = rc; + 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; + } + } + assert(pes==pesLim); + pes = sqlite3_realloc(pes, (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; } }