diff --git a/Makefile.in b/Makefile.in index 29fcaf75c0..2c899a1f4a 100644 --- a/Makefile.in +++ b/Makefile.in @@ -1513,3 +1513,99 @@ sqlite3.def: $(REAL_LIBOBJ) sqlite3.dll: $(REAL_LIBOBJ) sqlite3.def $(TCC) -shared -o $@ sqlite3.def \ -Wl,"--strip-all" $(REAL_LIBOBJ) + + +# +# fiddle/wasm section +# +fiddle_dir = ext/fiddle +fiddle_dir_abs = $(TOP)/$(fiddle_dir) +# ^^^ some emcc opts require absolute paths +fiddle_html = $(fiddle_dir)/fiddle.html +fiddle_module_js = $(fiddle_dir)/fiddle-module.js +fiddle_generated = $(fiddle_module_js) \ + $(fiddle_dir)/fiddle-module.wasm +sqlite3_wasm_js = $(fiddle_dir)/sqlite3.js +sqlite3_wasm = $(fiddle_dir)/sqlite3.wasm +sqlite3_wasm_generated = $(sqlite3_wasm) $(sqlite3_wasm_js) +clean-wasm: + rm -f $(fiddle_generated) $(sqlite3_wasm_generated) +clean: clean-wasm +#emcc_opt = -O0 +#emcc_opt = -O1 +#emcc_opt = -O2 +#emcc_opt = -O3 +emcc_opt = -Oz +emcc_flags = $(emcc_opt) -sALLOW_TABLE_GROWTH -I. $(SHELL_OPT) +$(fiddle_module_js): Makefile sqlite3.c shell.c \ + $(fiddle_dir)/EXPORTED_RUNTIME_METHODS \ + $(fiddle_dir)/EXPORTED_FUNCTIONS.fiddle + emcc -o $@ $(emcc_flags) \ + -sENVIRONMENT=web \ + -sMODULARIZE \ + -sEXPORT_NAME=initFiddleModule \ + -sEXPORTED_RUNTIME_METHODS=@$(fiddle_dir_abs)/EXPORTED_RUNTIME_METHODS \ + -sEXPORTED_FUNCTIONS=@$(fiddle_dir_abs)/EXPORTED_FUNCTIONS.fiddle \ + sqlite3.c shell.c +$(sqlite3_wasm_js): Makefile sqlite3.c \ + $(fiddle_dir)/sqlite3-api.js \ + $(fiddle_dir)/EXPORTED_RUNTIME_METHODS \ + $(fiddle_dir)/EXPORTED_FUNCTIONS.sqlite3-api + emcc -o $@ $(emcc_flags) \ + -sENVIRONMENT=web \ + -sMODULARIZE \ + -sEXPORT_NAME=initSqlite3Module \ + -sEXPORTED_RUNTIME_METHODS=@$(fiddle_dir_abs)/EXPORTED_RUNTIME_METHODS \ + -sEXPORTED_FUNCTIONS=@$(fiddle_dir_abs)/EXPORTED_FUNCTIONS.sqlite3-api \ + --post-js=$(fiddle_dir)/sqlite3-api.js \ + --no-entry \ + sqlite3.c +fiddle: $(fiddle_module_js) +sqlite3-wasm: $(sqlite3_wasm_js) +wasm: fiddle sqlite3-wasm +######################################################################## +# Explanation of the emcc build flags: +# +# -sENVIRONMENT=web: elides bootstrap code related to non-web JS +# environments like node.js. Removing this makes the output a tiny +# tick larger but hypothetically makes it more portable to +# non-browser JS environments. +# +# -sMODULARIZE: changes how the generated code is structured to avoid +# declaring a global Module object and instead installing a function +# which loads and initialized the module. The function is named... +# +# -sEXPORT_NAME=jsFunctionName (see -sMODULARIZE) +# +# -sEXPORTED_RUNTIME_METHODS=@/absolute/path/to/file: a file +# containing a list of emscripten-supplied APIs, one per line, which +# must be exported into the generated JS. Must be an absolute path! +# +# -sEXPORTED_FUNCTIONS=@/absolute/path/to/file: a file containing a +# list of C functions, one per line, which must be exported via wasm +# so they're visible to JS. C symbols names in that file must all +# start with an underscore for reasons known only to the emcc +# developers. e.g., _sqlite3_open_v2 and _sqlite3_finalize. Must be +# an absolute path! +# +# --no-entry: for compiling library code with no main(). If this is +# not supplied and the code has a main(), it is called as part of the +# module init process. Note that main() is #if'd out of shell.c +# (renamed) when building in wasm mode. +# +# --pre-js/--post-js=FILE relative or absolute paths to JS files to +# prepend/append to the emcc-generated bootstrapping JS. It's +# easier/faster to develop with separate JS files (reduces rebuilding +# requirements) but certain configurations, namely -sMODULARIZE, may +# require using at least a --pre-js file. They can be used +# individually and need not be paired. +# +# -O0..-O3 and -Oz: optimization levels affect not only C-style +# optimization but whether or not the resulting generated JS code +# gets minified. -O0 compiles _much_ more quickly than -O3 or -Oz, +# and doesn't minimize any JS code, so is recommended for +# development. -O3 or -Oz are recommended for deployment, but +# primarily because -Oz will shrink the wasm file notably. JS-side +# minification makes little difference in terms of overall +# distributable size. +######################################################################## diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.fiddle b/ext/fiddle/EXPORTED_FUNCTIONS.fiddle new file mode 100644 index 0000000000..b96ce4e67c --- /dev/null +++ b/ext/fiddle/EXPORTED_FUNCTIONS.fiddle @@ -0,0 +1,7 @@ +_fiddle_exec +_fiddle_interrupt +_fiddle_experiment +_fiddle_the_db +_fiddle_db_arg +_fiddle_db_filename +_fiddle_reset_db diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api new file mode 100644 index 0000000000..3127b294d9 --- /dev/null +++ b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api @@ -0,0 +1,51 @@ +_sqlite3_bind_blob +_sqlite3_bind_double +_sqlite3_bind_int +_sqlite3_bind_int64 +_sqlite3_bind_null +_sqlite3_bind_parameter_count +_sqlite3_bind_parameter_index +_sqlite3_bind_text +_sqlite3_changes +_sqlite3_clear_bindings +_sqlite3_close_v2 +_sqlite3_column_blob +_sqlite3_column_bytes +_sqlite3_column_count +_sqlite3_column_count +_sqlite3_column_double +_sqlite3_column_int +_sqlite3_column_int64 +_sqlite3_column_name +_sqlite3_column_text +_sqlite3_column_type +_sqlite3_compileoption_get +_sqlite3_compileoption_used +_sqlite3_create_function_v2 +_sqlite3_data_count +_sqlite3_db_filename +_sqlite3_errmsg +_sqlite3_exec +_sqlite3_finalize +_sqlite3_interrupt +_sqlite3_libversion +_sqlite3_open +_sqlite3_open_v2 +_sqlite3_prepare_v2 +_sqlite3_prepare_v2 +_sqlite3_reset +_sqlite3_result_blob +_sqlite3_result_double +_sqlite3_result_error +_sqlite3_result_int +_sqlite3_result_null +_sqlite3_result_text +_sqlite3_sourceid +_sqlite3_sql +_sqlite3_step +_sqlite3_value_blob +_sqlite3_value_bytes +_sqlite3_value_double +_sqlite3_value_text +_sqlite3_value_type +_free diff --git a/ext/fiddle/EXPORTED_RUNTIME_METHODS b/ext/fiddle/EXPORTED_RUNTIME_METHODS new file mode 100644 index 0000000000..763722d236 --- /dev/null +++ b/ext/fiddle/EXPORTED_RUNTIME_METHODS @@ -0,0 +1,14 @@ +ALLOC_NORMAL +FS +UTF8ToString +addFunction +allocate +allocateUTF8OnStack +ccall +cwrap +getValue +removeFunction +setValue +stackAlloc +stackRestore +stackSave diff --git a/ext/fiddle/Makefile b/ext/fiddle/Makefile new file mode 100644 index 0000000000..6f3279b61a --- /dev/null +++ b/ext/fiddle/Makefile @@ -0,0 +1,33 @@ +# This GNU makefile exists primarily to simplify/speed up development +# from emacs. It is not part of the canonical build process. +default: + $(MAKE) -C ../.. wasm -e emcc_opt=-O0 + +clean: + $(MAKE) -C ../../ clean-wasm + +fiddle_files = emscripten.css fiddle.html \ + fiddle.js fiddle-module.js \ + fiddle-module.wasm fiddle-worker.js + +# fiddle_remote is the remote destination for the fiddle app. It +# must be a [user@]HOST:/path for rsync. +# Note that the target "should probably" contain a symlink of +# index.html -> fiddle.html. +fiddle_remote ?= +ifeq (,$(fiddle_remote)) +ifneq (,$(wildcard /home/stephan)) + fiddle_remote = wh2:www/wh/sqlite3/. +else ifneq (,$(wildcard /home/drh)) + #fiddle_remote = if appropriate, add that user@host:/path here +endif +endif + +$(fiddle_files): default + +push-fiddle: $(fiddle_files) + @if [ x = "x$(fiddle_remote)" ]; then \ + echo "fiddle_remote must be a [user@]HOST:/path for rsync"; \ + exit 1; \ + fi + rsync -va $(fiddle_files) $(fiddle_remote) diff --git a/ext/fiddle/SqliteTestUtil.js b/ext/fiddle/SqliteTestUtil.js new file mode 100644 index 0000000000..cf78946dc7 --- /dev/null +++ b/ext/fiddle/SqliteTestUtil.js @@ -0,0 +1,144 @@ +/* + 2022-05-22 + + 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 bootstrapping code used by various test scripts + which live in this file's directory. +*/ +(function(){ + /* querySelectorAll() proxy */ + const EAll = function(/*[element=document,] cssSelector*/){ + return (arguments.length>1 ? arguments[0] : document) + .querySelectorAll(arguments[arguments.length-1]); + }; + /* querySelector() proxy */ + const E = function(/*[element=document,] cssSelector*/){ + return (arguments.length>1 ? arguments[0] : document) + .querySelector(arguments[arguments.length-1]); + }; + + /** + Helpers for writing sqlite3-specific tests. + */ + self/*window or worker*/.SqliteTestUtil = { + /** Running total of the number of tests run via + this API. */ + counter: 0, + /** + If expr is a function, it is called and its result + is returned, coerced to a bool, else expr, coerced to + a bool, is returned. + */ + toBool: function(expr){ + return (expr instanceof Function) ? !!expr() : !!expr; + }, + /** abort() if expr is false. If expr is a function, it + is called and its result is evaluated. + */ + assert: function f(expr, msg){ + if(!f._){ + f._ = ('undefined'===typeof abort + ? (msg)=>{throw new Error(msg)} + : abort); + } + ++this.counter; + if(!this.toBool(expr)){ + f._(msg || "Assertion failed."); + } + return this; + }, + /** Identical to assert() but throws instead of calling + abort(). */ + affirm: function(expr, msg){ + ++this.counter; + if(!this.toBool(expr)) throw new Error(msg || "Affirmation failed."); + return this; + }, + /** Calls f() and squelches any exception it throws. If it + does not throw, this function throws. */ + mustThrow: function(f, msg){ + ++this.counter; + let err; + try{ f(); } catch(e){err=e;} + if(!err) throw new Error(msg || "Expected exception."); + return this; + }, + /** Throws if expr is truthy or expr is a function and expr() + returns truthy. */ + throwIf: function(expr, msg){ + ++this.counter; + if(this.toBool(expr)) throw new Error(msg || "throwIf() failed"); + return this; + }, + /** Throws if expr is falsy or expr is a function and expr() + returns falsy. */ + throwUnless: function(expr, msg){ + ++this.counter; + if(!this.toBool(expr)) throw new Error(msg || "throwUnless() failed"); + return this; + } + }; + + + /** + This is a module object for use with the emscripten-installed + initSqlite3Module() factory function. + */ + self.sqlite3TestModule = { + postRun: [ + /* function(theModule){...} */ + ], + //onRuntimeInitialized: function(){}, + /* Proxy for C-side stdout output. */ + print: function(){ + console.log.apply(console, Array.prototype.slice.call(arguments)); + }, + /* Proxy for C-side stderr output. */ + printErr: function(){ + console.error.apply(console, Array.prototype.slice.call(arguments)); + }, + /** + Called by the module init bits to report loading + progress. It gets passed an empty argument when loading is + done (after onRuntimeInitialized() and any this.postRun + callbacks have been run). + */ + setStatus: function f(text){ + if(!f.last){ + f.last = { text: '', step: 0 }; + f.ui = { + status: E('#module-status'), + progress: E('#module-progress'), + spinner: E('#module-spinner') + }; + } + if(text === f.last.text) return; + f.last.text = text; + if(f.ui.progress){ + f.ui.progress.value = f.last.step; + f.ui.progress.max = f.last.step + 1; + } + ++f.last.step; + if(text) { + f.ui.status.classList.remove('hidden'); + f.ui.status.innerText = text; + }else{ + if(f.ui.progress){ + f.ui.progress.remove(); + f.ui.spinner.remove(); + delete f.ui.progress; + delete f.ui.spinner; + } + f.ui.status.classList.add('hidden'); + } + } + }; +})(self/*window or worker*/); diff --git a/ext/fiddle/emscripten.css b/ext/fiddle/emscripten.css new file mode 100644 index 0000000000..7e3dc811d0 --- /dev/null +++ b/ext/fiddle/emscripten.css @@ -0,0 +1,24 @@ +/* emcscript-related styling, used during the module load/intialization processes... */ +.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; } +div.emscripten { text-align: center; } +div.emscripten_border { border: 1px solid black; } +#module-spinner { overflow: visible; } +#module-spinner > * { + margin-top: 1em; +} +.spinner { + height: 50px; + width: 50px; + margin: 0px auto; + animation: rotation 0.8s linear infinite; + border-left: 10px solid rgb(0,150,240); + border-right: 10px solid rgb(0,150,240); + border-bottom: 10px solid rgb(0,150,240); + border-top: 10px solid rgb(100,0,200); + border-radius: 100%; + background-color: rgb(200,100,250); +} +@keyframes rotation { + from {transform: rotate(0deg);} + to {transform: rotate(360deg);} +} diff --git a/ext/fiddle/fiddle-worker.js b/ext/fiddle/fiddle-worker.js new file mode 100644 index 0000000000..e71955a8a8 --- /dev/null +++ b/ext/fiddle/fiddle-worker.js @@ -0,0 +1,300 @@ +/* + 2022-05-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 is the JS Worker file for the sqlite3 fiddle app. It loads the + sqlite3 wasm module and offers access to the db via the Worker + message-passing interface. + + Forewarning: this API is still very much Under Construction and + subject to any number of changes as experience reveals what those + need to be. + + Because we can have only a single message handler, as opposed to an + arbitrary number of discrete event listeners like with DOM elements, + we have to define a lower-level message API. Messages abstractly + look like: + + { type: string, data: type-specific value } + + Where 'type' is used for dispatching and 'data' is a + 'type'-dependent value. + + The 'type' values expected by each side of the main/worker + connection vary. The types are described below but subject to + change at any time as this experiment evolves. + + Workers-to-Main types + + - stdout, stderr: indicate stdout/stderr output from the wasm + layer. The data property is the string of the output, noting + that the emscripten binding emits these one line at a time. Thus, + if a C-side puts() emits multiple lines in a single call, the JS + side will see that as multiple calls. Example: + + {type:'stdout', data: 'Hi, world.'} + + - module: Status text. This is intended to alert the main thread + about module loading status so that, e.g., the main thread can + update a progress widget and DTRT when the module is finished + loading and available for work. Status messages come in the form + + {type:'module', data:{ + type:'status', + data: {text:string|null, step:1-based-integer} + } + + with an incrementing step value for each subsequent message. When + the module loading is complete, a message with a text value of + null is posted. + + - working: data='start'|'end'. Indicates that work is about to be + sent to the module or has just completed. This can be used, e.g., + to disable UI elements which should not be activated while work + is pending. Example: + + {type:'working', data:'start'} + + Main-to-Worker types: + + - shellExec: data=text to execute as if it had been entered in the + sqlite3 CLI shell app (as opposed to sqlite3_exec()). This event + causes the worker to emit a 'working' event (data='start') before + it starts and a 'working' event (data='end') when it finished. If + called while work is currently being executed it emits stderr + message instead of doing actual work, as the underlying db cannot + handle concurrent tasks. Example: + + {type:'shellExec', data: 'select * from sqlite_master'} + + - More TBD as the higher-level db layer develops. +*/ + +/* + Apparent browser(s) bug: console messages emitted may be duplicated + in the console, even though they're provably only run once. See: + + https://stackoverflow.com/questions/49659464 + + Noting that it happens in Firefox as well as Chrome. Harmless but + annoying. +*/ +"use strict"; +(function(){ + /** + Posts a message in the form {type,data} unless passed more than 2 + args, in which case it posts {type, data:[arg1...argN]}. + */ + const wMsg = function(type,data){ + postMessage({ + type, + data: arguments.length<3 + ? data + : Array.prototype.slice.call(arguments,1) + }); + }; + + const stdout = function(){wMsg('stdout', Array.prototype.slice.call(arguments));}; + const stderr = function(){wMsg('stderr', Array.prototype.slice.call(arguments));}; + + self.onerror = function(/*message, source, lineno, colno, error*/) { + const err = arguments[4]; + if(err && 'ExitStatus'==err.name){ + /* This is relevant for the sqlite3 shell binding but not the + lower-level binding. */ + fiddleModule.isDead = true; + stderr("FATAL ERROR:", err.message); + stderr("Restarting the app requires reloading the page."); + wMsg('error', err); + } + console.error(err); + fiddleModule.setStatus('Exception thrown, see JavaScript console: '+err); + }; + + const Sqlite3Shell = { + /** Returns the name of the currently-opened db. */ + dbFilename: function f(){ + if(!f._) f._ = fiddleModule.cwrap('fiddle_db_filename', "string", ['string']); + return f._(); + }, + /** + Runs the given text through the shell as if it had been typed + in by a user. Fires a working/start event before it starts and + working/end event when it finishes. + */ + exec: function f(sql){ + if(!f._) f._ = fiddleModule.cwrap('fiddle_exec', null, ['string']); + if(fiddleModule.isDead){ + stderr("shell module has exit()ed. Cannot run SQL."); + return; + } + wMsg('working','start'); + try { + if(f._running){ + stderr('Cannot run multiple commands concurrently.'); + }else{ + f._running = true; + f._(sql); + } + } finally { + delete f._running; + wMsg('working','end'); + } + }, + resetDb: function f(){ + if(!f._) f._ = fiddleModule.cwrap('fiddle_reset_db', null); + stdout("Resetting database."); + f._(); + stdout("Reset",this.dbFilename()); + }, + /* Interrupt can't work: this Worker is tied up working, so won't get the + interrupt event which would be needed to perform the interrupt. */ + interrupt: function f(){ + if(!f._) f._ = fiddleModule.cwrap('fiddle_interrupt', null); + stdout("Requesting interrupt."); + f._(); + } + }; + + self.onmessage = function f(ev){ + ev = ev.data; + if(!f.cache){ + f.cache = { + prevFilename: null + }; + } + //console.debug("worker: onmessage.data",ev); + switch(ev.type){ + case 'shellExec': Sqlite3Shell.exec(ev.data); return; + case 'db-reset': Sqlite3Shell.resetDb(); return; + case 'interrupt': Sqlite3Shell.interrupt(); return; + /** Triggers the export of the current db. Fires an + event in the form: + + {type:'db-export', + data:{ + filename: name of db, + buffer: contents of the db file (Uint8Array), + error: on error, a message string and no buffer property. + } + } + */ + case 'db-export': { + const fn = Sqlite3Shell.dbFilename(); + stdout("Exporting",fn+"."); + const fn2 = fn ? fn.split(/[/\\]/).pop() : null; + try{ + if(!fn2) throw new Error("DB appears to be closed."); + wMsg('db-export',{ + filename: fn2, + buffer: fiddleModule.FS.readFile(fn, {encoding:"binary"}) + }); + }catch(e){ + /* Post a failure message so that UI elements disabled + during the export can be re-enabled. */ + wMsg('db-export',{ + filename: fn, + error: e.message + }); + } + return; + } + case 'open': { + /* Expects: { + buffer: ArrayBuffer | Uint8Array, + filename: for logging/informational purposes only + } */ + const opt = ev.data; + let buffer = opt.buffer; + if(buffer instanceof Uint8Array){ + }else if(buffer instanceof ArrayBuffer){ + buffer = new Uint8Array(buffer); + }else{ + stderr("'open' expects {buffer:Uint8Array} containing an uploaded db."); + return; + } + const fn = ( + opt.filename + ? opt.filename.split(/[/\\]/).pop().replace('"','_') + : ("db-"+((Math.random() * 10000000) | 0)+ + "-"+((Math.random() * 10000000) | 0)+".sqlite3") + ); + /* We cannot delete the existing db file until the new one + is installed, which means that we risk overflowing our + quota (if any) by having both the previous and current + db briefly installed in the virtual filesystem. */ + fiddleModule.FS.createDataFile("/", fn, buffer, true, true); + const oldName = Sqlite3Shell.dbFilename(); + Sqlite3Shell.exec('.open "/'+fn+'"'); + if(oldName !== fn){ + fiddleModule.FS.unlink(oldName); + } + stdout("Replaced DB with",fn+"."); + return; + } + }; + console.warn("Unknown fiddle-worker message type:",ev); + }; + + /** + emscripten module for use with build mode -sMODULARIZE. + */ + const fiddleModule = { + print: stdout, + printErr: stderr, + /** + Intercepts status updates from the emscripting module init + and fires worker events with a type of 'status' and a + payload of: + + { + text: string | null, // null at end of load process + step: integer // starts at 1, increments 1 per call + } + + We have no way of knowing in advance how many steps will + be processed/posted, so creating a "percentage done" view is + not really practical. One can be approximated by giving it a + current value of message.step and max value of message.step+1, + though. + + When work is finished, a message with a text value of null is + submitted. + + After a message with text==null is posted, the module may later + post messages about fatal problems, e.g. an exit() being + triggered, so it is recommended that UI elements for posting + status messages not be outright removed from the DOM when + text==null, and that they instead be hidden until/unless + text!=null. + */ + setStatus: function f(text){ + if(!f.last) f.last = { step: 0, text: '' }; + else if(text === f.last.text) return; + f.last.text = text; + wMsg('module',{ + type:'status', + data:{step: ++f.last.step, text: text||null} + }); + } + }; + + importScripts('fiddle-module.js'); + /** + initFiddleModule() is installed via fiddle-module.js due to + building with: + + emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule + */ + initFiddleModule(fiddleModule).then(function(thisModule){ + wMsg('fiddle-ready'); + }); +})(); diff --git a/ext/fiddle/fiddle.html b/ext/fiddle/fiddle.html new file mode 100644 index 0000000000..e461f65220 --- /dev/null +++ b/ext/fiddle/fiddle.html @@ -0,0 +1,264 @@ + + + + + + sqlite3 fiddle + + + + + + + +
sqlite3 fiddle
+ +
+
+
Initializing app...
+
+ On a slow internet connection this may take a moment. If this + message displays for "a long time", intialization may have + failed and the JavaScript console may contain clues as to why. +
+
+
Downloading...
+
+ +
+ + + +
+
+ Options +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + + +
+
+
+ +
+ + + +
+
+
+
+ + + + diff --git a/ext/fiddle/fiddle.js b/ext/fiddle/fiddle.js new file mode 100644 index 0000000000..c03a09af11 --- /dev/null +++ b/ext/fiddle/fiddle.js @@ -0,0 +1,783 @@ +/* + 2022-05-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 is the main entry point for the sqlite3 fiddle app. It sets up the + various UI bits, loads a Worker for the db connection, and manages the + communication between the UI and worker. +*/ +(function(){ + 'use strict'; + /* Recall that the 'self' symbol, except where locally + overwritten, refers to the global window or worker object. */ + + const storage = (function(NS/*namespace object in which to store this module*/){ + /* Pedantic licensing note: this code originated in the Fossil SCM + source tree, where it has a different license, but the person who + ported it into sqlite is the same one who wrote it for fossil. */ + 'use strict'; + NS = NS||{}; + + /** + This module provides a basic wrapper around localStorage + or sessionStorage or a dummy proxy object if neither + of those are available. + */ + const tryStorage = function f(obj){ + if(!f.key) f.key = 'storage.access.check'; + try{ + obj.setItem(f.key, 'f'); + const x = obj.getItem(f.key); + obj.removeItem(f.key); + if(x!=='f') throw new Error(f.key+" failed") + return obj; + }catch(e){ + return undefined; + } + }; + + /** Internal storage impl for this module. */ + const $storage = + tryStorage(window.localStorage) + || tryStorage(window.sessionStorage) + || tryStorage({ + // A basic dummy xyzStorage stand-in + $$$:{}, + setItem: function(k,v){this.$$$[k]=v}, + getItem: function(k){ + return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined; + }, + removeItem: function(k){delete this.$$$[k]}, + clear: function(){this.$$$={}} + }); + + /** + For the dummy storage we need to differentiate between + $storage and its real property storage for hasOwnProperty() + to work properly... + */ + const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage; + + /** + A prefix which gets internally applied to all storage module + property keys so that localStorage and sessionStorage across the + same browser profile instance do not "leak" across multiple apps + being hosted by the same origin server. Such cross-polination is + still there but, with this key prefix applied, it won't be + immediately visible via the storage API. + + With this in place we can justify using localStorage instead of + sessionStorage. + + One implication of using localStorage and sessionStorage is that + their scope (the same "origin" and client application/profile) + allows multiple apps on the same origin to use the same + storage. Thus /appA/foo could then see changes made via + /appB/foo. The data do not cross user- or browser boundaries, + though, so it "might" arguably be called a + feature. storageKeyPrefix was added so that we can sandbox that + state for each separate app which shares an origin. + + See: https://fossil-scm.org/forum/forumpost/4afc4d34de + + Sidebar: it might seem odd to provide a key prefix and stick all + properties in the topmost level of the storage object. We do that + because adding a layer of object to sandbox each app would mean + (de)serializing that whole tree on every storage property change. + e.g. instead of storageObject.projectName.foo we have + storageObject[storageKeyPrefix+'foo']. That's soley for + efficiency's sake (in terms of battery life and + environment-internal storage-level effort). + */ + const storageKeyPrefix = ( + $storageHolder===$storage/*localStorage or sessionStorage*/ + ? ( + (NS.config ? + (NS.config.projectCode || NS.config.projectName + || NS.config.shortProjectName) + : false) + || window.location.pathname + )+'::' : ( + '' /* transient storage */ + ) + ); + + /** + A proxy for localStorage or sessionStorage or a + page-instance-local proxy, if neither one is availble. + + Which exact storage implementation is uses is unspecified, and + apps must not rely on it. + */ + NS.storage = { + storageKeyPrefix: storageKeyPrefix, + /** Sets the storage key k to value v, implicitly converting + it to a string. */ + set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v), + /** Sets storage key k to JSON.stringify(v). */ + setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)), + /** Returns the value for the given storage key, or + dflt if the key is not found in the storage. */ + get: (k,dflt)=>$storageHolder.hasOwnProperty( + storageKeyPrefix+k + ) ? $storage.getItem(storageKeyPrefix+k) : dflt, + /** Returns true if the given key has a value of "true". If the + key is not found, it returns true if the boolean value of dflt + is "true". (Remember that JS persistent storage values are all + strings.) */ + getBool: function(k,dflt){ + return 'true'===this.get(k,''+(!!dflt)); + }, + /** Returns the JSON.parse()'d value of the given + storage key's value, or dflt is the key is not + found or JSON.parse() fails. */ + getJSON: function f(k,dflt){ + try { + const x = this.get(k,f); + return x===f ? dflt : JSON.parse(x); + } + catch(e){return dflt} + }, + /** Returns true if the storage contains the given key, + else false. */ + contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k), + /** Removes the given key from the storage. Returns this. */ + remove: function(k){ + $storage.removeItem(storageKeyPrefix+k); + return this; + }, + /** Clears ALL keys from the storage. Returns this. */ + clear: function(){ + this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k)); + return this; + }, + /** Returns an array of all keys currently in the storage. */ + keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)), + /** Returns true if this storage is transient (only available + until the page is reloaded), indicating that fileStorage + and sessionStorage are unavailable. */ + isTransient: ()=>$storageHolder!==$storage, + /** Returns a symbolic name for the current storage mechanism. */ + storageImplName: function(){ + if($storage===window.localStorage) return 'localStorage'; + else if($storage===window.sessionStorage) return 'sessionStorage'; + else return 'transient'; + }, + + /** + Returns a brief help text string for the currently-selected + storage type. + */ + storageHelpDescription: function(){ + return { + localStorage: "Browser-local persistent storage with an "+ + "unspecified long-term lifetime (survives closing the browser, "+ + "but maybe not a browser upgrade).", + sessionStorage: "Storage local to this browser tab, "+ + "lost if this tab is closed.", + "transient": "Transient storage local to this invocation of this page." + }[this.storageImplName()]; + } + }; + return NS.storage; + })({})/*storage API setup*/; + + + /** Name of the stored copy of SqliteFiddle.config. */ + const configStorageKey = 'sqlite3-fiddle-config'; + + /** + The SqliteFiddle object is intended to be the primary + app-level object for the main-thread side of the sqlite + fiddle application. It uses a worker thread to load the + sqlite WASM module and communicate with it. + */ + const SF/*local convenience alias*/ + = window.SqliteFiddle/*canonical name*/ = { + /* Config options. */ + config: { + /* If true, SqliteFiddle.echo() will auto-scroll the + output widget to the bottom when it receives output, + else it won't. */ + autoScrollOutput: true, + /* If true, the output area will be cleared before each + command is run, else it will not. */ + autoClearOutput: false, + /* If true, SqliteFiddle.echo() will echo its output to + the console, in addition to its normal output widget. + That slows it down but is useful for testing. */ + echoToConsole: false, + /* If true, display input/output areas side-by-side. */ + sideBySide: false, + /* If true, swap positions of the input/output areas. */ + swapInOut: false + }, + /** + Emits the given text, followed by a line break, to the + output widget. If given more than one argument, they are + join()'d together with a space between each. As a special + case, if passed a single array, that array is used in place + of the arguments array (this is to facilitate receiving + lists of arguments via worker events). + */ + echo: function f(text) { + /* Maintenance reminder: we currently require/expect a textarea + output element. It might be nice to extend this to behave + differently if the output element is a non-textarea element, + in which case it would need to append the given text as a TEXT + node and add a line break. */ + if(!f._){ + f._ = document.getElementById('output'); + f._.value = ''; // clear browser cache + } + if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); + else if(1===arguments.length && Array.isArray(text)) text = text.join(' '); + // These replacements are necessary if you render to raw HTML + //text = text.replace(/&/g, "&"); + //text = text.replace(//g, ">"); + //text = text.replace('\n', '
', 'g'); + if(null===text){/*special case: clear output*/ + f._.value = ''; + return; + }else if(this.echo._clearPending){ + delete this.echo._clearPending; + f._.value = ''; + } + if(this.config.echoToConsole) console.log(text); + if(this.jqTerm) this.jqTerm.echo(text); + f._.value += text + "\n"; + if(this.config.autoScrollOutput){ + f._.scrollTop = f._.scrollHeight; + } + }, + _msgMap: {}, + /** Adds a worker message handler for messages of the given + type. */ + addMsgHandler: function f(type,callback){ + if(Array.isArray(type)){ + type.forEach((t)=>this.addMsgHandler(t, callback)); + return this; + } + (this._msgMap.hasOwnProperty(type) + ? this._msgMap[type] + : (this._msgMap[type] = [])).push(callback); + return this; + }, + /** Given a worker message, runs all handlers for msg.type. */ + runMsgHandlers: function(msg){ + const list = (this._msgMap.hasOwnProperty(msg.type) + ? this._msgMap[msg.type] : false); + if(!list){ + console.warn("No handlers found for message type:",msg); + return false; + } + //console.debug("runMsgHandlers",msg); + list.forEach((f)=>f(msg)); + return true; + }, + /** Removes all message handlers for the given message type. */ + clearMsgHandlers: function(type){ + delete this._msgMap[type]; + return this; + }, + /* Posts a message in the form {type, data} to the db worker. Returns this. */ + wMsg: function(type,data){ + this.worker.postMessage({type, data}); + return this; + }, + /** + Prompts for confirmation and, if accepted, deletes + all content and tables in the (transient) database. + */ + resetDb: function(){ + if(window.confirm("Really destroy all content and tables " + +"in the (transient) db?")){ + this.wMsg('db-reset'); + } + return this; + }, + /** Stores this object's config in the browser's storage. */ + storeConfig: function(){ + storage.setJSON(configStorageKey,this.config); + } + }; + + if(1){ /* Restore SF.config */ + const storedConfig = storage.getJSON(configStorageKey); + if(storedConfig){ + /* Copy all properties to SF.config which are currently in + storedConfig. We don't bother copying any other + properties: those have been removed from the app in the + meantime. */ + Object.keys(SF.config).forEach(function(k){ + if(storedConfig.hasOwnProperty(k)){ + SF.config[k] = storedConfig[k]; + } + }); + } + } + + SF.worker = new Worker('fiddle-worker.js'); + SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data); + SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data)); + + /* querySelectorAll() proxy */ + const EAll = function(/*[element=document,] cssSelector*/){ + return (arguments.length>1 ? arguments[0] : document) + .querySelectorAll(arguments[arguments.length-1]); + }; + /* querySelector() proxy */ + const E = function(/*[element=document,] cssSelector*/){ + return (arguments.length>1 ? arguments[0] : document) + .querySelector(arguments[arguments.length-1]); + }; + + /** Handles status updates from the Module object. */ + SF.addMsgHandler('module', function f(ev){ + ev = ev.data; + if('status'!==ev.type){ + console.warn("Unexpected module-type message:",ev); + return; + } + if(!f.ui){ + f.ui = { + status: E('#module-status'), + progress: E('#module-progress'), + spinner: E('#module-spinner') + }; + } + const msg = ev.data; + if(f.ui.progres){ + progress.value = msg.step; + progress.max = msg.step + 1/*we don't know how many steps to expect*/; + } + if(1==msg.step){ + f.ui.progress.classList.remove('hidden'); + f.ui.spinner.classList.remove('hidden'); + } + if(msg.text){ + f.ui.status.classList.remove('hidden'); + f.ui.status.innerText = msg.text; + }else{ + if(f.ui.progress){ + f.ui.progress.remove(); + f.ui.spinner.remove(); + delete f.ui.progress; + delete f.ui.spinner; + } + f.ui.status.classList.add('hidden'); + /* The module can post messages about fatal problems, + e.g. an exit() being triggered or assertion failure, + after the last "load" message has arrived, so + leave f.ui.status and message listener intact. */ + } + }); + + /** + The 'fiddle-ready' event is fired (with no payload) when the + wasm module has finished loading. Interestingly, that happens + _before_ the final module:status event */ + SF.addMsgHandler('fiddle-ready', function(){ + SF.clearMsgHandlers('fiddle-ready'); + self.onSFLoaded(); + }); + + /** + Performs all app initialization which must wait until after the + worker module is loaded. This function removes itself when it's + called. + */ + self.onSFLoaded = function(){ + delete this.onSFLoaded; + // Unhide all elements which start out hidden + EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden')); + E('#btn-reset').addEventListener('click',()=>SF.resetDb()); + const taInput = E('#input'); + const btnClearIn = E('#btn-clear'); + btnClearIn.addEventListener('click',function(){ + taInput.value = ''; + },false); + // Ctrl-enter and shift-enter both run the current SQL. + taInput.addEventListener('keydown',function(ev){ + if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){ + ev.preventDefault(); + ev.stopPropagation(); + btnShellExec.click(); + } + }, false); + const taOutput = E('#output'); + const btnClearOut = E('#btn-clear-output'); + btnClearOut.addEventListener('click',function(){ + taOutput.value = ''; + if(SF.jqTerm) SF.jqTerm.clear(); + },false); + const btnShellExec = E('#btn-shell-exec'); + btnShellExec.addEventListener('click',function(ev){ + let sql; + ev.preventDefault(); + if(taInput.selectionStart e.addEventListener('click', cmdClick, false) + ); + + btnInterrupt.addEventListener('click',function(){ + SF.wMsg('interrupt'); + }); + + /** Initiate a download of the db. */ + const btnExport = E('#btn-export'); + const eDisableDuringExport = [ + /* UI elements to disable while export is running. Normally + the export is fast enough that this won't matter, but we + really don't want to be reading (from outside of sqlite) + the db when the user taps btnShellExec. */ + btnShellExec, btnExport + ]; + btnExport.addEventListener('click',function(){ + eDisableDuringExport.forEach(e=>e.setAttribute('disabled','disabled')); + SF.wMsg('db-export'); + }); + SF.addMsgHandler('db-export', function(ev){ + eDisableDuringExport.forEach(e=>e.removeAttribute('disabled')); + ev = ev.data; + if(ev.error){ + SF.echo("Export failed:",ev.error); + return; + } + const blob = new Blob([ev.buffer], {type:"application/x-sqlite3"}); + const a = document.createElement('a'); + document.body.appendChild(a); + a.href = window.URL.createObjectURL(blob); + a.download = ev.filename; + a.addEventListener('click',function(){ + setTimeout(function(){ + SF.echo("Exported (possibly auto-downloaded):",ev.filename); + window.URL.revokeObjectURL(a.href); + a.remove(); + },500); + }); + a.click(); + }); + /** + Handle load/import of an external db file. + */ + E('#load-db').addEventListener('change',function(){ + const f = this.files[0]; + const r = new FileReader(); + const status = {loaded: 0, total: 0}; + this.setAttribute('disabled','disabled'); + r.addEventListener('loadstart', function(){ + SF.echo("Loading",f.name,"..."); + }); + r.addEventListener('progress', function(ev){ + SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes."); + }); + const that = this; + r.addEventListener('load', function(){ + that.removeAttribute('disabled'); + SF.echo("Loaded",f.name+". Opening db..."); + SF.wMsg('open',{ + filename: f.name, + buffer: this.result + }); + }); + r.addEventListener('error',function(){ + that.removeAttribute('disabled'); + SF.echo("Loading",f.name,"failed for unknown reasons."); + }); + r.addEventListener('abort',function(){ + that.removeAttribute('disabled'); + SF.echo("Cancelled loading of",f.name+"."); + }); + r.readAsArrayBuffer(f); + }); + + EAll('.fieldset.collapsible').forEach(function(fs){ + const legend = E(fs,'span.legend'), + content = EAll(fs,':scope > div'); + legend.addEventListener('click', function(){ + fs.classList.toggle('collapsed'); + content.forEach((d)=>d.classList.toggle('hidden')); + }, false); + }); + + /** + Given a DOM element, this routine measures its "effective + height", which is the bounding top/bottom range of this element + and all of its children, recursively. For some DOM structure + cases, a parent may have a reported height of 0 even though + children have non-0 sizes. + + Returns 0 if !e or if the element really has no height. + */ + const effectiveHeight = function f(e){ + if(!e) return 0; + if(!f.measure){ + f.measure = function callee(e, depth){ + if(!e) return; + const m = e.getBoundingClientRect(); + if(0===depth){ + callee.top = m.top; + callee.bottom = m.bottom; + }else{ + callee.top = m.top ? Math.min(callee.top, m.top) : callee.top; + callee.bottom = Math.max(callee.bottom, m.bottom); + } + Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1)); + if(0===depth){ + //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top)); + f.extra += callee.bottom - callee.top; + } + return f.extra; + }; + } + f.extra = 0; + f.measure(e,0); + return f.extra; + }; + + /** + Returns a function, that, as long as it continues to be invoked, + will not be triggered. The function will be called after it stops + being called for N milliseconds. If `immediate` is passed, call + the callback immediately and hinder future invocations until at + least the given time has passed. + + If passed only 1 argument, or passed a falsy 2nd argument, + the default wait time set in this function's $defaultDelay + property is used. + + Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function + */ + const debounce = function f(func, wait, immediate) { + var timeout; + if(!wait) wait = f.$defaultDelay; + return function() { + const context = this, args = Array.prototype.slice.call(arguments); + const later = function() { + timeout = undefined; + if(!immediate) func.apply(context, args); + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if(callNow) func.apply(context, args); + }; + }; + debounce.$defaultDelay = 500 /*arbitrary*/; + + const ForceResizeKludge = (function(){ + /* Workaround for Safari mayhem regarding use of vh CSS + units.... We cannot use vh units to set the main view + size because Safari chokes on that, so we calculate + that height here. Larger than ~95% is too big for + Firefox on Android, causing the input area to move + off-screen. */ + const appViews = EAll('.app-view'); + const elemsToCount = [ + /* Elements which we need to always count in the + visible body size. */ + E('body > header'), + E('body > footer') + ]; + const resized = function f(){ + if(f.$disabled) return; + const wh = window.innerHeight; + var ht; + var extra = 0; + elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false); + ht = wh - extra; + appViews.forEach(function(e){ + e.style.height = + e.style.maxHeight = [ + "calc(", (ht>=100 ? ht : 100), "px", + " - 2em"/*fudge value*/,")" + /* ^^^^ hypothetically not needed, but both + Chrome/FF on Linux will force scrollbars on the + body if this value is too small. */ + ].join(''); + }); + }; + resized.$disabled = true/*gets deleted when setup is finished*/; + window.addEventListener('resize', debounce(resized, 250), false); + return resized; + })(); + + /** Set up a selection list of examples */ + (function(){ + const xElem = E('#select-examples'); + const examples = [ + {name: "Timer on", sql: ".timer on"}, + {name: "Setup table T", sql:`.nullvalue NULL +CREATE TABLE t(a,b); +INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012); +SELECT * FROM t;`}, + {name: "Table list", sql: ".tables"}, + {name: "Box Mode", sql: ".mode box"}, + {name: "JSON Mode", sql: ".mode json"}, + {name: "Mandlebrot", sql: `WITH RECURSIVE + xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2), + yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0), + m(iter, cx, cy, x, y) AS ( + SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis + UNION ALL + SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m + WHERE (x*x + y*y) < 4.0 AND iter<28 + ), + m2(iter, cx, cy) AS ( + SELECT max(iter), cx, cy FROM m GROUP BY cx, cy + ), + a(t) AS ( + SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') + FROM m2 GROUP BY cy + ) +SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;`} + ]; + const newOpt = function(lbl,val){ + const o = document.createElement('option'); + o.value = val; + if(!val) o.setAttribute('disabled',true); + o.appendChild(document.createTextNode(lbl)); + xElem.appendChild(o); + }; + newOpt("Examples (replaces input!)"); + examples.forEach((o)=>newOpt(o.name, o.sql)); + //xElem.setAttribute('disabled',true); + xElem.selectedIndex = 0; + xElem.addEventListener('change', function(){ + taInput.value = '-- ' + + this.selectedOptions[0].innerText + + '\n' + this.value; + SF.dbExec(this.value); + }); + })()/* example queries */; + + SF.echo(null/*clear any output generated by the init process*/); + if(window.jQuery && window.jQuery.terminal){ + /* Set up the terminal-style view... */ + const eTerm = window.jQuery('#view-terminal').empty(); + SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{ + prompt: 'sqlite> ', + greetings: false /* note that the docs incorrectly call this 'greeting' */ + }); + /* Set up a button to toggle the views... */ + const head = E('header#titlebar'); + const btnToggleView = document.createElement('button'); + btnToggleView.appendChild(document.createTextNode("Toggle View")); + head.appendChild(btnToggleView); + btnToggleView.addEventListener('click',function f(){ + EAll('.app-view').forEach(e=>e.classList.toggle('hidden')); + if(document.body.classList.toggle('terminal-mode')){ + ForceResizeKludge(); + } + }, false); + btnToggleView.click()/*default to terminal view*/; + } + SF.dbExec(null/*init the db and output the header*/); + SF.echo('This experimental app is provided in the hope that it', + 'may prove interesting or useful but is not an officially', + 'supported deliverable of the sqlite project. It is subject to', + 'any number of changes or outright removal at any time.\n'); + delete ForceResizeKludge.$disabled; + ForceResizeKludge(); + }/*onSFLoaded()*/; +})(); diff --git a/ext/fiddle/index.md b/ext/fiddle/index.md new file mode 100644 index 0000000000..9d1f8d83ea --- /dev/null +++ b/ext/fiddle/index.md @@ -0,0 +1,94 @@ +This directory houses a "fiddle"-style application which embeds a +[Web Assembly (WASM)](https://en.wikipedia.org/wiki/WebAssembly) +build of the sqlite3 shell app into an HTML page, effectively running +the shell in a client-side browser. + +It requires [emscripten][] and that the build environment be set up for +emscripten. A mini-HOWTO for setting that up follows... + +First, install the Emscripten SDK, as documented +[here](https://emscripten.org/docs/getting_started/downloads.html) and summarized +below for Linux environments: + +``` +# Clone the emscripten repository: +$ git clone https://github.com/emscripten-core/emsdk.git +$ cd emsdk + +# Download and install the latest SDK tools: +$ ./emsdk install latest + +# Make the "latest" SDK "active" for the current user: +$ ./emsdk activate latest +``` + +Those parts only need to be run once. The following needs to be run for each +shell instance which needs the `emcc` compiler: + +``` +# Activate PATH and other environment variables in the current terminal: +$ source ./emsdk_env.sh + +$ which emcc +/path/to/emsdk/upstream/emscripten/emcc +``` + +That `env` script needs to be sourced for building this application from the +top of the sqlite3 build tree: + +``` +$ make fiddle +``` + +Or: + +``` +$ cd ext/fiddle +$ make +``` + +That will generate the fiddle application under +[ext/fiddle](/dir/ext/fiddle), as `fiddle.html`. That application +cannot, due to XMLHttpRequest security limitations, run if the HTML +file is opened directly in the browser (i.e. if it is opened using a +`file://` URL), so it needs to be served via an HTTP server. For +example, using [althttpd][]: + +``` +$ cd ext/fiddle +$ althttpd -debug 1 -jail 0 -port 9090 -root . +``` + +Then browse to `http://localhost:9090/fiddle.html`. + +Note that when serving this app via [althttpd][], it must be a version +from 2022-05-17 or newer so that it recognizes the `.wasm` file +extension and responds with the mimetype `application/wasm`, as the +WASM loader is pedantic about that detail. + +# Known Quirks and Limitations + +Some "impedence mismatch" between C and WASM/JavaScript is to be +expected. + +## No I/O + +sqlite3 shell commands which require file I/O or pipes are disabled in +the WASM build. + +## `exit()` Triggered from C + +When C code calls `exit()`, as happens (for example) when running an +"unsafe" command when safe mode is active, WASM's connection to the +sqlite3 shell environment has no sensible choice but to shut down +because `exit()` leaves it in a state we can no longer recover +from. The JavaScript-side application attempts to recognize this and +warn the user that restarting the application is necessary. Currently +the only way to restart it is to reload the page. Restructuring the +shell code such that it could be "rebooted" without restarting the +JS app would require some invasive changes which are not currently +on any TODO list but have not been entirely ruled out long-term. + + +[emscripten]: https://emscripten.org +[althttpd]: https://sqlite.org/althttpd diff --git a/ext/fiddle/sqlite3-api.js b/ext/fiddle/sqlite3-api.js new file mode 100644 index 0000000000..d88ec9a8b8 --- /dev/null +++ b/ext/fiddle/sqlite3-api.js @@ -0,0 +1,1396 @@ +/* + 2022-05-22 + + 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 is intended to be appended to the emcc-generated + sqlite3.js via emcc: + + emcc ... -sMODULARIZE -sEXPORT_NAME=initSqlite3Module --post-js=THIS_FILE + + It is loaded by importing the emcc-generated sqlite3.js, then: + + initSqlite3Module({module object}).then( + function(theModule){ + theModule.sqlite3 == an object containing this file's + deliverables: + { + api: bindings for much of the core sqlite3 APIs, + SQLite3: high-level OO API wrapper + } + }); + + It is up to the caller to provide a module object compatible with + emcc, but it can be a plain empty object. The object passed to + initSqlite3Module() will get populated by the emscripten-generated + bits and, in part, by the code from this file. Specifically, this file + installs the `theModule.sqlite3` part shown above. + + The resulting sqlite3.api object wraps the standard sqlite3 C API in + a way as close to its native form as JS allows for. The + sqlite3.SQLite3 object provides a higher-level wrapper more + appropriate for general client-side use in JS. + + Because using certain parts of the low-level API properly requires + some degree of WASM-related magic, it is not recommended that that + API be used as-is in client-level code. Rather, client code should + use the higher-level OO API or write a custom wrapper on top of the + lower-level API. In short, most of the C-style API is used in an + intuitive manner from JS but any C-style APIs which take + pointers-to-pointer arguments require WASM-specific interfaces + installed by emcscripten-generated code. Those which take or return + only integers, doubles, strings, or "plain" pointers to db or + statement objects can be used in "as normal," noting that "pointers" + in wasm are simply 32-bit integers. + + # Goals and Non-goals of this API + + Goals: + + - Except where noted in the non-goals, provide a more-or-less + complete wrapper to the sqlite3 C API, insofar as WASM feature + parity with C allows for. In fact, provide at least 3... + + - (1) The aforementioned C-style API. (2) An OO-style API on + top of that, designed to run in the same thread (main window or + Web Worker) as the C API. (3) A less-capable wrapper which can + work across the main window/worker boundary, where the sqlite3 API + is one of those and this wrapper is in the other. That + constellation places some considerable limitations on how the API + can be interacted with, but keeping the DB operations out of the + UI thread is generally desirable. + + - Insofar as possible, support client-side storage using JS + filesystem APIs. As of this writing, such things are still very + much TODO. + + Non-goals: + + - As WASM is a web-centric technology and UTF-8 is the King of + Encodings in that realm, there are no current plans to support the + UTF16-related APIs. They would add a complication to the bindings + for no appreciable benefit. + + - Supporting old or niche-market platforms. WASM is built for a + modern web and requires modern platforms. + +*/ +if(!Module.postRun) Module.postRun = []; +/* ^^^^ the name Module is, in this setup, scope-local in the generated + file sqlite3.js, with which this file gets combined at build-time. */ +Module.postRun.push(function(namespace){ + 'use strict'; + /* For reference: sql.js does essentially everything we want and + it solves much of the wasm-related voodoo, but we'll need a + different structure because we want the db connection to run in + a worker thread and feed data back into the main + thread. Regardless of those differences, it makes a great point + of reference: + + https://github.com/sql-js/sql.js + + Some of the specific design goals here: + + - Bind a low-level sqlite3 API which is close to the native one + in terms of usage. + + - Create a higher-level one, more akin to sql.js and + node.js-style implementations. This one would speak directly + to the low-level API. This API could be used by clients who + import the low-level API directly into their main thread + (which we don't want to recommend but also don't want to + outright forbid). + + - Create a second higher-level one which speaks to the + low-level API via worker messages. This one would be intended + for use in the main thread, talking to the low-level UI via + worker messages. Because workers have only a single message + channel, some acrobatics will be needed here to feed async + work results back into client-side callbacks (as those + callbacks cannot simply be passed to the worker). Exactly + what those acrobatics should look like is not yet entirely + clear and much experimentation is pending. + */ + + const SQM = namespace/*the sqlite module object */; + + /** + Set up the main sqlite3 binding API here, mimicking the C API as + closely as we can. + + Attribution: though not a direct copy/paste, much of what + follows is strongly influenced by the sql.js implementation. + */ + const api = { + /* It is important that the following integer values match + those from the C code. Ideally we could fetch them from the + C API, e.g., in the form of a JSON object, but getting that + JSON string constructed within our current confines is + currently not worth the effort. + + Reminder to self: we could probably do so by adding the + proverbial level of indirection, calling in to C to get it, + and having that C func call an + emscripten-installed/JS-implemented library function which + builds the result object: + + const obj = {}; + sqlite3__get_enum(function(key,val){ + obj[key] = val; + }); + + but whether or not we can pass a function that way, via a + (void*) is as yet unknown. + */ + /* Minimum subset of sqlite result codes we'll need. */ + SQLITE_OK: 0, + SQLITE_ROW: 100, + SQLITE_DONE: 101, + /* sqlite data types */ + SQLITE_INTEGER: 1, + SQLITE_FLOAT: 2, + SQLITE_TEXT: 3, + SQLITE_BLOB: 4, + SQLITE_NULL: 5, + /* create_function() flags */ + SQLITE_DETERMINISTIC: 0x000000800, + SQLITE_DIRECTONLY: 0x000080000, + SQLITE_INNOCUOUS: 0x000200000, + /* sqlite encodings, used for creating UDFs, noting that we + will only support UTF8. */ + SQLITE_UTF8: 1 + }; + const cwrap = SQM.cwrap; + [/* C-side functions to bind. Each entry is an array with 3 or 4 + elements: + + ["c-side name", + "result type" (cwrap() syntax), + [arg types in cwrap() syntax] + ] + + If it has 4 elements, the first one is an alternate name to + use for the JS-side binding. That's required when overloading + a binding for two different uses. + */ + ["sqlite3_bind_blob","number",["number", "number", "number", "number", "number"]], + ["sqlite3_bind_double","number",["number", "number", "number"]], + ["sqlite3_bind_int","number",["number", "number", "number"]], + /*Noting that JS/wasm combo does not currently support 64-bit integers: + ["sqlite3_bind_int64","number",["number", "number", "number"]],*/ + ["sqlite3_bind_null","void",["number"]], + ["sqlite3_bind_parameter_count", "number", ["number"]], + ["sqlite3_bind_parameter_index","number",["number", "string"]], + ["sqlite3_bind_text","number",["number", "number", "number", "number", "number"]], + ["sqlite3_changes", "number", ["number"]], + ["sqlite3_clear_bindings","number",["number"]], + ["sqlite3_close_v2", "number", ["number"]], + ["sqlite3_column_blob","number", ["number", "number"]], + ["sqlite3_column_bytes","number",["number", "number"]], + ["sqlite3_column_count", "number", ["number"]], + ["sqlite3_column_count","number",["number"]], + ["sqlite3_column_double","number",["number", "number"]], + ["sqlite3_column_int","number",["number", "number"]], + /*Noting that JS/wasm combo does not currently support 64-bit integers: + ["sqlite3_column_int64","number",["number", "number"]],*/ + ["sqlite3_column_name","string",["number", "number"]], + ["sqlite3_column_text","string",["number", "number"]], + ["sqlite3_column_type","number",["number", "number"]], + ["sqlite3_compileoption_get", "string", ["number"]], + ["sqlite3_compileoption_used", "number", ["string"]], + ["sqlite3_create_function_v2", "number", + ["number", "string", "number", "number","number", + "number", "number", "number", "number"]], + ["sqlite3_data_count", "number", ["number"]], + ["sqlite3_db_filename", "string", ["number", "string"]], + ["sqlite3_errmsg", "string", ["number"]], + ["sqlite3_exec", "number", ["number", "string", "number", "number", "number"]], + ["sqlite3_finalize", "number", ["number"]], + ["sqlite3_interrupt", "void", ["number"]], + ["sqlite3_libversion", "string", []], + ["sqlite3_open", "number", ["string", "number"]], + //["sqlite3_open_v2", "number", ["string", "number", "number", "string"]], + //^^^^ TODO: add the flags needed for the 3rd arg + ["sqlite3_prepare_v2", "number", ["number", "string", "number", "number", "number"]], + ["sqlite3_prepare_v2_sqlptr", "sqlite3_prepare_v2", + /* Impl which requires that the 2nd argument be a pointer to + the SQL, instead of a string. This is used for cases where + we require a non-NULL value for the final argument. We may + or may not need this, depending on how our higher-level + API shapes up, but this code's spiritual guide (sql.js) + uses it we we'll include it. */ + "number", ["number", "number", "number", "number", "number"]], + ["sqlite3_reset", "number", ["number"]], + ["sqlite3_result_blob",null,["number", "number", "number", "number"]], + ["sqlite3_result_double",null,["number", "number"]], + ["sqlite3_result_error",null,["number", "string", "number"]], + ["sqlite3_result_int",null,["number", "number"]], + ["sqlite3_result_null",null,["number"]], + ["sqlite3_result_text",null,["number", "string", "number", "number"]], + ["sqlite3_sourceid", "string", []], + ["sqlite3_sql", "string", ["number"]], + ["sqlite3_step", "number", ["number"]], + ["sqlite3_value_blob", "number", ["number"]], + ["sqlite3_value_bytes","number",["number"]], + ["sqlite3_value_double","number",["number"]], + ["sqlite3_value_text", "string", ["number"]], + ["sqlite3_value_type", "number", ["number"]] + //["sqlite3_normalized_sql", "string", ["number"]] + ].forEach(function(a){ + const k = (4==a.length) ? a.shift() : a[0]; + api[k] = cwrap.apply(this, a); + }); + + /* What follows is colloquially known as "OO API #1". It is a + binding of the sqlite3 API which is designed to be run within + the same thread (main or worker) as the one in which the + sqlite3 WASM binding was initialized. This wrapper cannot use + the sqlite3 binding if, e.g., the wrapper is in the main thread + and the sqlite3 API is in a worker. */ + + /** Memory for use in some pointer-to-pointer-passing routines. */ + const pPtrArg = stackAlloc(4); + /** Throws a new error, concatenating all args with a space between + each. */ + const toss = function(){ + throw new Error(Array.prototype.join.call(arguments, ' ')); + }; + + /** + The DB class wraps a sqlite3 db handle. + + It accepts the following argument signatures: + + - () + - (undefined) (same effect as ()) + - (Uint8Array holding an sqlite3 db image) + + It always generates a random filename and sets is to + the `filename` property of this object. + + Developer's note: the reason it does not (any longer) support + ":memory:" as a name is because we can apparently only export + images of DBs which are stored in the pseudo-filesystem + provided by the JS APIs. Since exporting and importing images + is an important usability feature for this class, ":memory:" + DBs are not supported (until/unless we can find a way to export + those as well). The naming semantics will certainly evolve as + this API does. + */ + const DB = function(arg){ + const fn = "db-"+((Math.random() * 10000000) | 0)+ + "-"+((Math.random() * 10000000) | 0)+".sqlite3"; + let buffer; + if(name instanceof Uint8Array){ + buffer = arg; + arg = undefined; + }else if(arguments.length && undefined!==arg){ + toss("Invalid arguments to DB constructor.", + "Expecting no args, undefined, or a", + "sqlite3 file as a Uint8Array."); + } + if(buffer){ + FS.createDataFile("/", fn, buffer, true, true); + } + setValue(pPtrArg, 0, "i32"); + this.checkRc(api.sqlite3_open(fn, pPtrArg)); + this._pDb = getValue(pPtrArg, "i32"); + this.filename = fn; + this._statements = {/*map of open Stmt _pointers_ to Stmt*/}; + this._udfs = {/*map of UDF names to wasm function _pointers_*/}; + }; + + /** + Internal-use enum for mapping JS types to DB-bindable types. + These do not (and need not) line up with the SQLITE_type + values. All values in this enum must be truthy and distinct + but they need not be numbers. + */ + const BindTypes = { + null: 1, + number: 2, + string: 3, + boolean: 4, + blob: 5 + }; + BindTypes['undefined'] == BindTypes.null; + + /** + This class wraps sqlite3_stmt. Calling this constructor + directly will trigger an exception. Use DB.prepare() to create + new instances. + */ + const Stmt = function(){ + if(BindTypes!==arguments[2]){ + toss("Do not call the Stmt constructor directly. Use DB.prepare()."); + } + this.db = arguments[0]; + this._pStmt = arguments[1]; + this.columnCount = api.sqlite3_column_count(this._pStmt); + this.parameterCount = api.sqlite3_bind_parameter_count(this._pStmt); + this._allocs = [/*list of alloc'd memory blocks for bind() values*/] + }; + + /** Throws if the given DB has been closed, else it is returned. */ + const affirmDbOpen = function(db){ + if(!db._pDb) toss("DB has been closed."); + return db; + }; + + /** Returns true if n is a 32-bit (signed) integer, + else false. */ + const isInt32 = function(n){ + return (n===n|0 && n<0xFFFFFFFF) ? true : undefined; + }; + + /** + Expects to be passed (arguments) from DB.exec() and + DB.execMulti(). Does the argument processing/validation, throws + on error, and returns a new object on success: + + { sql: the SQL, obt: optionsObj, cbArg: function} + + cbArg is only set if the opt.callback is set, in which case + it's a function which expects to be passed the current Stmt + and returns the callback argument of the type indicated by + the input arguments. + */ + const parseExecArgs = function(args){ + const out = {}; + switch(args.length){ + case 1: + if('string'===typeof args[0]){ + out.sql = args[0]; + out.opt = {}; + }else if(args[0] && 'object'===typeof args[0]){ + out.opt = args[0]; + out.sql = out.opt.sql; + } + break; + case 2: + out.sql = args[0]; + out.opt = args[1]; + break; + default: toss("Invalid argument count for exec()."); + }; + if('string'!==typeof out.sql) toss("Missing SQL argument."); + if(out.opt.callback){ + switch((undefined===out.opt.rowMode) + ? 'stmt' : out.opt.rowMode) { + case 'object': out.cbArg = (stmt)=>stmt.get({}); break; + case 'array': out.cbArg = (stmt)=>stmt.get([]); break; + case 'stmt': out.cbArg = (stmt)=>stmt; break; + default: toss("Invalid rowMode:",out.opt.rowMode); + } + } + return out; + }; + + /** If object opts has _its own_ property named p then that + property's value is returned, else dflt is returned. */ + const getOwnOption = (opts, p, dflt)=> + opts.hasOwnProperty(p) ? opts[p] : dflt; + + DB.prototype = { + /** + Expects to be given an sqlite3 API result code. If it is + falsy, this function returns this object, else it throws an + exception with an error message from sqlite3_errmsg(), + using this object's db handle. Note that if it's passed a + non-error code like SQLITE_ROW or SQLITE_DONE, it will + still throw but the error string might be "Not an error." + The various non-0 non-error codes need to be checked for in + client code where they are expected. + */ + checkRc: function(sqliteResultCode){ + if(!sqliteResultCode) return this; + toss("sqlite result code",sqliteResultCode+":", + api.sqlite3_errmsg(this._pDb) || "Unknown db error."); + }, + /** + Finalizes all open statements and closes this database + connection. This is a no-op if the db has already been + closed. + */ + close: function(){ + if(this._pDb){ + let s; + const that = this; + Object.keys(this._statements).forEach(function(k,s){ + delete that._statements[k]; + if(s && s._pStmt) s.finalize(); + }); + Object.values(this._udfs).forEach(SQM.removeFunction); + delete this._udfs; + delete this._statements; + delete this.filename; + api.sqlite3_close_v2(this._pDb); + delete this._pDb; + } + }, + /** + Similar to this.filename but will return NULL for + special names like ":memory:". Not of much use until + we have filesystem support. Throws if the DB has + been closed. If passed an argument it then it will return + the filename of the ATTACHEd db with that name, else it assumes + a name of `main`. + */ + fileName: function(dbName){ + return api.sqlite3_db_filename(affirmDbOpen(this)._pDb, dbName||"main"); + }, + /** + Compiles the given SQL and returns a prepared Stmt. This is + the only way to create new Stmt objects. Throws on error. + */ + prepare: function(sql){ + affirmDbOpen(this); + setValue(pPtrArg,0,"i32"); + this.checkRc(api.sqlite3_prepare_v2(this._pDb, sql, -1, pPtrArg, null)); + const pStmt = getValue(pPtrArg, "i32"); + if(!pStmt) toss("Empty SQL is not permitted."); + const stmt = new Stmt(this, pStmt, BindTypes); + this._statements[pStmt] = stmt; + return stmt; + }, + /** + This function works like execMulti(), and takes the same + arguments, but is more efficient (performs much less work) + when the input SQL is only a single statement. If passed a + multi-statement SQL, it only processes the first one. + + This function supports one additional option not used by + execMulti(): + + - .multi: if true, this function acts as a proxy for + execMulti(). + */ + exec: function(/*(sql [,optionsObj]) or (optionsObj)*/){ + affirmDbOpen(this); + const arg = parseExecArgs(arguments); + if(!arg.sql) return this; + else if(arg.opt.multi){ + return this.execMulti(arg, undefined, BindTypes); + } + const opt = arg.opt; + let stmt; + try { + stmt = this.prepare(arg.sql); + if(opt.bind) stmt.bind(opt.bind); + if(opt.callback){ + while(stmt.step()){ + stmt._isLocked = true; + opt.callback(arg.cbArg(stmt), stmt); + stmt._isLocked = false; + } + }else{ + stmt.step(); + } + }finally{ + if(stmt){ + delete stmt._isLocked; + stmt.finalize(); + } + } + return this; + + }/*exec()*/, + /** + Executes one or more SQL statements. Its arguments + must be either (sql,optionsObject) or (optionsObject). + In the latter case, optionsObject.sql must contain the + SQL to execute. Returns this object. Throws on error. + + If no SQL is provided, or a non-string is provided, an + exception is triggered. Empty SQL, on the other hand, is + simply a no-op. + + The optional options object may contain any of the following + properties: + + - .sql = the SQL to run (unless it's provided as the first + argument). + + - .bind = a single value valid as an argument for + Stmt.bind(). This is ONLY applied to the FIRST non-empty + statement in the SQL which has any bindable + parameters. (Empty statements are skipped entirely.) + + - .callback = a function which gets called for each row of + the FIRST statement in the SQL (if it has any result + rows). The second argument passed to the callback is + always the current Stmt object (so that the caller + may collect column names, or similar). The first + argument passed to the callback defaults to the current + Stmt object but may be changed with ... + + - .rowMode = a string describing what type of argument + should be passed as the first argument to the callback. A + value of 'object' causes the results of `stmt.get({})` to + be passed to the object. A value of 'array' causes the + results of `stmt.get([])` to be passed to the callback. + A value of 'stmt' is equivalent to the default, passing + the current Stmt to the callback (noting that it's always + passed as the 2nd argument). Any other value triggers an + exception. + + - saveSql = an optional array. If set, the SQL of each + executed statement is appended to this array before the + statement is executed (but after it is prepared - we + don't have the string until after that). Empty SQL + statements are elided. + + ACHTUNG #1: The callback MUST NOT modify the Stmt + object. Calling any of the Stmt.get() variants, + Stmt.getColumnName(), or simililar, is legal, but calling + step() or finalize() is not. Routines which are illegal + in this context will trigger an exception. + + ACHTUNG #2: The semantics of the `bind` and `callback` + options may well change or those options may be removed + altogether for this function (but retained for exec()). + */ + execMulti: function(/*(sql [,obj]) || (obj)*/){ + affirmDbOpen(this); + const arg = (BindTypes===arguments[2] + /* ^^^ Being passed on from exec() */ + ? arguments[0] : parseExecArgs(arguments)); + if(!arg.sql) return this; + const opt = arg.opt; + const stack = stackSave(); + let stmt; + let bind = opt.bind; + let rowMode = ( + (opt.callback && opt.rowMode) + ? opt.rowMode : false); + try{ + let pSql = SQM.allocateUTF8OnStack(arg.sql) + const pzTail = stackAlloc(4); + while(getValue(pSql, "i8")){ + setValue(pPtrArg, 0, "i32"); + setValue(pzTail, 0, "i32"); + this.checkRc(api.sqlite3_prepare_v2_sqlptr( + this._pDb, pSql, -1, pPtrArg, pzTail + )); + const pStmt = getValue(pPtrArg, "i32"); + pSql = getValue(pzTail, "i32"); + if(!pStmt) continue; + if(opt.saveSql){ + opt.saveSql.push(api.sqlite3_sql(pStmt).trim()); + } + stmt = new Stmt(this, pStmt, BindTypes); + if(bind && stmt.parameterCount){ + stmt.bind(bind); + bind = null; + } + if(opt.callback && null!==rowMode){ + while(stmt.step()){ + stmt._isLocked = true; + callback(arg.cbArg(stmt), stmt); + stmt._isLocked = false; + } + rowMode = null; + }else{ + // Do we need to while(stmt.step()){} here? + stmt.step(); + } + stmt.finalize(); + stmt = null; + } + }finally{ + if(stmt){ + delete stmt._isLocked; + stmt.finalize(); + } + stackRestore(stack); + } + return this; + }/*execMulti()*/, + /** + Creates a new scalar UDF (User-Defined Function) which is + accessible via SQL code. This function may be called in any + of the following forms: + + - (name, function) + - (name, function, optionsObject) + - (name, optionsObject) + - (optionsObject) + + In the final two cases, the function must be defined as the + 'callback' property of the options object. In the final + case, the function's name must be the 'name' property. + + This can only be used to create scalar functions, not + aggregate or window functions. UDFs cannot be removed from + a DB handle after they're added. + + On success, returns this object. Throws on error. + + When called from SQL, arguments to the UDF, and its result, + will be converted between JS and SQL with as much fidelity + as is feasible, triggering an exception if a type + conversion cannot be determined. Some freedom is afforded + to numeric conversions due to friction between the JS and C + worlds: integers which are larger than 32 bits will be + treated as doubles, as JS does not support 64-bit integers + and it is (as of this writing) illegal to use WASM + functions which take or return 64-bit integers from JS. + + The optional options object may contain flags to modify how + the function is defined: + + - .arity: the number of arguments which SQL calls to this + function expect or require. The default value is the + callback's length property (i.e. the number of declared + parameters it has). A value of -1 means that the function + is variadic and may accept any number of arguments, up to + sqlite3's compile-time limits. sqlite3 will enforce the + argument count if is zero or greater. + + The following properties correspond to flags documented at: + + https://sqlite.org/c3ref/create_function.html + + - .deterministic = SQLITE_DETERMINISTIC + - .directOnly = SQLITE_DIRECTONLY + - .innocuous = SQLITE_INNOCUOUS + + + Maintenance reminder: the ability to add new + WASM-accessible functions to the runtime requires that the + WASM build is compiled with emcc's `-sALLOW_TABLE_GROWTH` + flag. + */ + createFunction: function f(name, callback,opt){ + switch(arguments.length){ + case 1: /* (optionsObject) */ + opt = name; + name = opt.name; + callback = opt.callback; + break; + case 2: /* (name, callback|optionsObject) */ + if(!(callback instanceof Function)){ + opt = callback; + callback = opt.callback; + } + break; + default: break; + } + if(!opt) opt = {}; + if(!(callback instanceof Function)){ + toss("Invalid arguments: expecting a callback function."); + }else if('string' !== typeof name){ + toss("Invalid arguments: missing function name."); + } + if(!f._extractArgs){ + /* Static init */ + f._extractArgs = function(argc, pArgv){ + let i, pVal, valType, arg; + const tgt = []; + for(i = 0; i < argc; ++i){ + pVal = getValue(pArgv + (4 * i), "i32"); + valType = api.sqlite3_value_type(pVal); + switch(valType){ + case api.SQLITE_INTEGER: + case api.SQLITE_FLOAT: + arg = api.sqlite3_value_double(pVal); + break; + case SQLITE_TEXT: + arg = api.sqlite3_value_text(pVal); + break; + case SQLITE_BLOB:{ + const n = api.sqlite3_value_bytes(ptr); + const pBlob = api.sqlite3_value_blob(ptr); + arg = new Uint8Array(n); + let i; + for(i = 0; i < n; ++i) arg[i] = HEAP8[pBlob+i]; + break; + } + default: + arg = null; break; + } + tgt.push(arg); + } + return tgt; + }/*_extractArgs()*/; + f._setResult = function(pCx, val){ + switch(typeof val) { + case 'boolean': + api.sqlite3_result_int(pCx, val ? 1 : 0); + break; + case 'number': { + (isInt32(val) + ? api.sqlite3_result_int + : api.sqlite3_result_double)(pCx, val); + break; + } + case 'string': + api.sqlite3_result_text(pCx, val, -1, + -1/*==SQLITE_TRANSIENT*/); + break; + case 'object': + if(null===val) { + api.sqlite3_result_null(pCx); + break; + }else if(undefined!==val.length){ + const pBlob = + SQM.allocate(val, SQM.ALLOC_NORMAL); + api.sqlite3_result_blob(pCx, pBlob, val.length, -1/*==SQLITE_TRANSIENT*/); + SQM._free(blobptr); + break; + } + // else fall through + default: + toss("Don't not how to handle this UDF result value:",val); + }; + }/*_setResult()*/; + }/*static init*/ + const wrapper = function(pCx, argc, pArgv){ + try{ + f._setResult(pCx, callback.apply(null, f._extractArgs(argc, pArgv))); + }catch(e){ + api.sqlite3_result_error(pCx, e.message, -1); + } + }; + const pUdf = SQM.addFunction(wrapper, "viii"); + let fFlags = 0; + if(getOwnOption(opt, 'deterministic')) fFlags |= api.SQLITE_DETERMINISTIC; + if(getOwnOption(opt, 'directOnly')) fFlags |= api.SQLITE_DIRECTONLY; + if(getOwnOption(opt, 'innocuous')) fFlags |= api.SQLITE_INNOCUOUS; + name = name.toLowerCase(); + try { + this.checkRc(api.sqlite3_create_function_v2( + this._pDb, name, + (opt.hasOwnProperty('arity') ? +opt.arity : callback.length), + api.SQLITE_UTF8 | fFlags, null/*pApp*/, pUdf, + null/*xStep*/, null/*xFinal*/, null/*xDestroy*/)); + }catch(e){ + SQM.removeFunction(pUdf); + throw e; + } + if(this._udfs.hasOwnProperty(name)){ + SQM.removeFunction(this._udfs[name]); + } + this._udfs[name] = pUdf; + return this; + }/*createFunction()*/, + /** + Prepares the given SQL, step()s it one time, and returns + the value of the first result column. If it has no results, + undefined is returned. If passed a second argument, it is + treated like an argument to Stmt.bind(), so may be any type + supported by that function. Throws on error (e.g. malformed + SQL). + */ + selectValue: function(sql,bind){ + let stmt, rc; + try { + stmt = this.prepare(sql).bind(bind); + if(stmt.step()) rc = stmt.get(0); + }finally{ + if(stmt) stmt.finalize(); + } + return rc; + }, + + /** + Exports a copy of this db's file as a Uint8Array and + returns it. It is technically not legal to call this while + any prepared statement are currently active. Throws if this + db is not open. + + Maintenance reminder: the corresponding sql.js impl of this + feature closes the current db, finalizing any active + statements and (seemingly unnecessarily) destroys any UDFs, + copies the file, and then re-opens it (without restoring + the UDFs). Those gymnastics are not necessary on the tested + platform but might be necessary on others. Because of that + eventuality, this interface currently enforces that no + statements are active when this is run. It will throw if + any are. + */ + exportBinaryImage: function(){ + affirmDbOpen(this); + if(Object.keys(this._statements).length){ + toss("Cannot export with prepared statements active!", + "finalize() all statements and try again."); + } + const img = FS.readFile(this.filename, {encoding:"binary"}); + return img; + } + }/*DB.prototype*/; + + + /** Throws if the given Stmt has been finalized, else stmt is + returned. */ + const affirmStmtOpen = function(stmt){ + if(!stmt._pStmt) toss("Stmt has been closed."); + return stmt; + }; + + /** Returns an opaque truthy value from the BindTypes + enum if v's type is a valid bindable type, else + returns a falsy value. As a special case, a value of + undefined is treated as a bind type of null. */ + const isSupportedBindType = function(v){ + let t = BindTypes[(null===v||undefined===v) ? 'null' : typeof v]; + switch(t){ + case BindTypes.boolean: + case BindTypes.null: + case BindTypes.number: + case BindTypes.string: + return t; + default: + if(v instanceof Uint8Array) return BindTypes.blob; + return undefined; + } + }; + + /** + If isSupportedBindType(v) returns a truthy value, this + function returns that value, else it throws. + */ + const affirmSupportedBindType = function(v){ + return isSupportedBindType(v) || toss("Unsupport bind() argument type."); + }; + + /** + If key is a number and within range of stmt's bound parameter + count, key is returned. + + If key is not a number then it is checked against named + parameters. If a match is found, its index is returned. + + Else it throws. + */ + const affirmParamIndex = function(stmt,key){ + const n = ('number'===typeof key) + ? key : api.sqlite3_bind_parameter_index(stmt._pStmt, key); + if(0===n || (n===key && (n!==(n|0)/*floating point*/))){ + toss("Invalid bind() parameter name: "+key); + } + else if(n<1 || n>stmt.parameterCount) toss("Bind index",key,"is out of range."); + return n; + }; + + /** Throws if ndx is not an integer or if it is out of range + for stmt.columnCount, else returns stmt. + + Reminder: this will also fail after the statement is finalized + but the resulting error will be about an out-of-bounds column + index. + */ + const affirmColIndex = function(stmt,ndx){ + if((ndx !== (ndx|0)) || ndx<0 || ndx>=stmt.columnCount){ + toss("Column index",ndx,"is out of range."); + } + return stmt; + }; + + /** + If stmt._isLocked is truthy, this throws an exception + complaining that the 2nd argument (an operation name, + e.g. "bind()") is not legal while the statement is "locked". + Locking happens before an exec()-like callback is passed a + statement, to ensure that the callback does not mutate or + finalize the statement. If it does not throw, it returns stmt. + */ + const affirmUnlocked = function(stmt,currentOpName){ + if(stmt._isLocked){ + toss("Operation is illegal when statement is locked:",currentOpName); + } + return stmt; + }; + + /** + Binds a single bound parameter value on the given stmt at the + given index (numeric or named) using the given bindType (see + the BindTypes enum) and value. Throws on error. Returns stmt on + success. + */ + const bindOne = function f(stmt,ndx,bindType,val){ + affirmUnlocked(stmt, 'bind()'); + if(!f._){ + f._ = { + string: function(stmt, ndx, val, asBlob){ + const bytes = intArrayFromString(val,true); + const pStr = SQM.allocate(bytes, ALLOC_NORMAL); + stmt._allocs.push(pStr); + const func = asBlob ? api.sqlite3_bind_blob : api.sqlite3_bind_text; + return func(stmt._pStmt, ndx, pStr, bytes.length, 0); + } + }; + } + affirmSupportedBindType(val); + ndx = affirmParamIndex(stmt,ndx); + let rc = 0; + switch((null===val || undefined===val) ? BindTypes.null : bindType){ + case BindTypes.null: + rc = api.sqlite3_bind_null(stmt._pStmt, ndx); + break; + case BindTypes.string:{ + rc = f._.string(stmt, ndx, val, false); + break; + } + case BindTypes.number: { + const m = (isInt32(val) + ? api.sqlite3_bind_int + /*It's illegal to bind a 64-bit int + from here*/ + : api.sqlite3_bind_double); + rc = m(stmt._pStmt, ndx, val); + break; + } + case BindTypes.boolean: + rc = api.sqlite3_bind_int(stmt._pStmt, ndx, val ? 1 : 0); + break; + case BindTypes.blob: { + if('string'===typeof val){ + rc = f._.string(stmt, ndx, val, true); + }else{ + const len = val.length; + if(undefined===len){ + toss("Binding a value as a blob requires", + "that it have a length member."); + } + const pBlob = SQM.allocate(val, ALLOC_NORMAL); + stmt._allocs.push(pBlob); + rc = api.sqlite3_bind_blob(stmt._pStmt, ndx, pBlob, len, 0); + } + } + default: toss("Unsupported bind() argument type."); + } + if(rc) stmt.db.checkRc(rc); + return stmt; + }; + + /** Frees any memory explicitly allocated for the given + Stmt object. Returns stmt. */ + const freeBindMemory = function(stmt){ + let m; + while(undefined !== (m = stmt._allocs.pop())){ + SQM._free(m); + } + return stmt; + }; + + Stmt.prototype = { + /** + "Finalizes" this statement. This is a no-op if the + statement has already been finalizes. Returns + undefined. Most methods in this class will throw if called + after this is. + */ + finalize: function(){ + if(this._pStmt){ + affirmUnlocked(this,'finalize()'); + freeBindMemory(this); + delete this.db._statements[this._pStmt]; + api.sqlite3_finalize(this._pStmt); + delete this.columnCount; + delete this.parameterCount; + delete this._pStmt; + delete this.db; + delete this._isLocked; + } + }, + /** Clears all bound values. Returns this object. + Throws if this statement has been finalized. */ + clearBindings: function(){ + freeBindMemory( + affirmUnlocked(affirmStmtOpen(this), 'clearBindings()') + ); + api.sqlite3_clear_bindings(this._pStmt); + this._mayGet = false; + return this; + }, + /** + Resets this statement so that it may be step()ed again + from the beginning. Returns this object. Throws if this + statement has been finalized. + + If passed a truthy argument then this.clearBindings() is + also called, otherwise any existing bindings, along with + any memory allocated for them, are retained. + */ + reset: function(alsoClearBinds){ + affirmUnlocked(this,'reset()'); + if(alsoClearBinds) this.clearBindings(); + api.sqlite3_reset(affirmStmtOpen(this)._pStmt); + this._mayGet = false; + return this; + }, + /** + Binds one or more values to its bindable parameters. It + accepts 1 or 2 arguments: + + If passed a single argument, it must be either an array, an + object, or a value of a bindable type (see below). + + If passed 2 arguments, the first one is the 1-based bind + index or bindable parameter name and the second one must be + a value of a bindable type. + + Bindable value types: + + - null is bound as NULL. + + - undefined as a standalone value is a no-op intended to + simplify certain client-side use cases: passing undefined + as a value to this function will not actually bind + anything and this function will skip confirmation that + binding is even legal. (Those semantics simplify certain + client-side uses.) Conversely, a value of undefined as an + array or object property when binding an array/object + (see below) is treated the same as null. + + - Numbers are bound as either doubles or integers: doubles + if they are larger than 32 bits, else double or int32, + depending on whether they have a fractional part. (It is, + as of this writing, illegal to call (from JS) a WASM + function which either takes or returns an int64.) + Booleans are bound as integer 0 or 1. It is not expected + the distinction of binding doubles which have no + fractional parts is integers is significant for the + majority of clients due to sqlite3's data typing + model. This API does not currently support the BigInt + type. + + - Strings are bound as strings (use bindAsBlob() to force + blob binding). + + - Uint8Array instances are bound as blobs. + + If passed an array, each element of the array is bound at + the parameter index equal to the array index plus 1 + (because arrays are 0-based but binding is 1-based). + + If passed an object, each object key is treated as a + bindable parameter name. The object keys _must_ match any + bindable parameter names, including any `$`, `@`, or `:` + prefix. Because `$` is a legal identifier chararacter in + JavaScript, that is the suggested prefix for bindable + parameters. + + It returns this object on success and throws on + error. Errors include: + + - Any bind index is out of range, a named bind parameter + does not match, or this statement has no bindable + parameters. + + - Any value to bind is of an unsupported type. + + - Passed no arguments or more than two. + + - The statement has been finalized. + */ + bind: function(/*[ndx,] arg*/){ + affirmStmtOpen(this); + let ndx, arg; + switch(arguments.length){ + case 1: ndx = 1; arg = arguments[0]; break; + case 2: ndx = arguments[0]; arg = arguments[1]; break; + default: toss("Invalid bind() arguments."); + } + if(undefined===arg){ + /* It might seem intuitive to bind undefined as NULL + but this approach simplifies certain client-side + uses when passing on arguments between 2+ levels of + functions. */ + return this; + }else if(!this.parameterCount){ + toss("This statement has no bindable parameters."); + } + this._mayGet = false; + if(null===arg){ + /* bind NULL */ + return bindOne(this, ndx, BindTypes.null, arg); + } + else if(Array.isArray(arg)){ + /* bind each entry by index */ + if(1!==arguments.length){ + toss("When binding an array, an index argument is not permitted."); + } + arg.forEach((v,i)=>bindOne(this, i+1, affirmSupportedBindType(v), v)); + return this; + } + else if('object'===typeof arg/*null was checked above*/){ + /* bind by name */ + if(1!==arguments.length){ + toss("When binding an object, an index argument is not permitted."); + } + Object.keys(arg) + .forEach(k=>bindOne(this, k, + affirmSupportedBindType(arg[k]), + arg[k])); + return this; + }else{ + return bindOne(this, ndx, + affirmSupportedBindType(arg), arg); + } + toss("Should not reach this point."); + }, + /** + Special case of bind() which binds the given value + using the BLOB binding mechanism instead of the default + selected one for the value. The ndx may be a numbered + or named bind index. The value must be of type string, + Uint8Array, or null/undefined (both treated as null). + + If passed a single argument, a bind index of 1 is assumed. + */ + bindAsBlob: function(ndx,arg){ + affirmStmtOpen(this); + if(1===arguments.length){ + ndx = 1; + arg = arguments[0]; + } + const t = affirmSupportedBindType(arg); + if(BindTypes.string !== t && BindTypes.blob !== t + && BindTypes.null !== t){ + toss("Invalid value type for bindAsBlob()"); + } + this._mayGet = false; + return bindOne(this, ndx, BindTypes.blob, arg); + }, + /** + Steps the statement one time. If the result indicates that + a row of data is available, true is returned. If no row of + data is available, false is returned. Throws on error. + */ + step: function(){ + affirmUnlocked(this, 'step()'); + const rc = api.sqlite3_step(affirmStmtOpen(this)._pStmt); + switch(rc){ + case api.SQLITE_DONE: return this._mayGet = false; + case api.SQLITE_ROW: return this._mayGet = true; + default: + this._mayGet = false; + console.warn("sqlite3_step() rc=",rc,"SQL =", + api.sqlite3_sql(this._pStmt)); + this.db.checkRc(rc); + }; + }, + /** + Fetches the value from the given 0-based column index of + the current data row, throwing if index is out of range. + + Requires that step() has just returned a truthy value, else + an exception is thrown. + + By default it will determine the data type of the result + automatically. If passed a second arugment, it must be one + of the enumeration values for sqlite3 types, which are + defined as members of the sqlite3 module: SQLITE_INTEGER, + SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB. Any other value, + except for undefined, will trigger an exception. Passing + undefined is the same as not passing a value. It is legal + to, e.g., fetch an integer value as a string, in which case + sqlite3 will convert the value to a string. + + If ndx is an array, this function behaves a differently: it + assigns the indexes of the array, from 0 to the number of + result columns, to the values of the corresponding column, + and returns that array. + + If ndx is a plain object, this function behaves even + differentlier: it assigns the properties of the object to + the values of their corresponding result columns. + + Blobs are returned as Uint8Array instances. + + Potential TODO: add type ID SQLITE_JSON, which fetches the + result as a string and passes it (if it's not null) to + JSON.parse(), returning the result of that. Until then, + getJSON() can be used for that. + */ + get: function(ndx,asType){ + if(!affirmStmtOpen(this)._mayGet){ + toss("Stmt.step() has not (recently) returned true."); + } + if(Array.isArray(ndx)){ + let i = 0; + while(i32bits */ + } + case api.SQLITE_FLOAT: + return api.sqlite3_column_double(this._pStmt, ndx); + case api.SQLITE_TEXT: + return api.sqlite3_column_text(this._pStmt, ndx); + case api.SQLITE_BLOB: { + const n = api.sqlite3_column_bytes(this._pStmt, ndx); + const ptr = api.sqlite3_column_blob(this._pStmt, ndx); + const rc = new Uint8Array(n); + for(let i = 0; i < n; ++i) rc[i] = HEAP8[ptr + i]; + return rc; + } + default: toss("Don't know how to translate", + "type of result column #"+ndx+"."); + } + abort("Not reached."); + }, + /** Equivalent to get(ndx) but coerces the result to an + integer. */ + getInt: function(ndx){return this.get(ndx,api.SQLITE_INTEGER)}, + /** Equivalent to get(ndx) but coerces the result to a + float. */ + getFloat: function(ndx){return this.get(ndx,api.SQLITE_FLOAT)}, + /** Equivalent to get(ndx) but coerces the result to a + string. */ + getString: function(ndx){return this.get(ndx,api.SQLITE_TEXT)}, + /** Equivalent to get(ndx) but coerces the result to a + Uint8Array. */ + getBlob: function(ndx){return this.get(ndx,api.SQLITE_BLOB)}, + /** + A convenience wrapper around get() which fetches the value + as a string and then, if it is not null, passes it to + JSON.parse(), returning that result. Throws if parsing + fails. If the result is null, null is returned. An empty + string, on the other hand, will trigger an exception. + */ + getJSON: function(ndx){ + const s = this.get(ndx, api.SQLITE_STRING); + return null===s ? s : JSON.parse(s); + }, + /** + Returns the result column name of the given index, or + throws if index is out of bounds or this statement has been + finalized. This can be used without having run step() + first. + */ + getColumnName: function(ndx){ + return api.sqlite3_column_name( + affirmColIndex(affirmStmtOpen(this),ndx)._pStmt, ndx + ); + }, + /** + If this statement potentially has result columns, this + function returns an array of all such names. If passed an + array, it is used as the target and all names are appended + to it. Returns the target array. Throws if this statement + cannot have result columns. This object's columnCount member + holds the number of columns. + */ + getColumnNames: function(tgt){ + affirmColIndex(affirmStmtOpen(this),0); + if(!tgt) tgt = []; + for(let i = 0; i < this.columnCount; ++i){ + tgt.push(api.sqlite3_column_name(this._pStmt, i)); + } + return tgt; + }, + /** + If this statement has named bindable parameters and the + given name matches one, its 1-based bind index is + returned. If no match is found, 0 is returned. If it has no + bindable parameters, the undefined value is returned. + */ + getParamIndex: function(name){ + return (affirmStmtOpen(this).parameterCount + ? api.sqlite3_bind_parameter_index(this._pStmt, name) + : undefined); + } + }/*Stmt.prototype*/; + + /** OO binding's namespace. */ + const SQLite3 = { + version: { + lib: api.sqlite3_libversion(), + ooApi: "0.0.1" + }, + DB, + Stmt, + /** + Reports whether a given compile-time option, named by the + given argument. It has several distinct uses: + + If optName is an array then it is expected to be a list of + compilation options and this function returns an object + which maps each such option to true or false, indicating + whether or not the given option was included in this + build. That object is returned. + + If optName is an object, its keys are expected to be + compilation options and this function sets each entry to + true or false. That object is returned. + + If passed no arguments then it returns an object mapping + all known compilation options to their compile-time values, + or boolean true if they are defined with no value. + + In all other cases it returns true if the given option was + active when when compiling the sqlite3 module, else false. + + Compile-time option names may optionally include their + "SQLITE_" prefix. When it returns an object of all options, + the prefix is elided. + */ + compileOptionUsed: function f(optName){ + if(!arguments.length){ + if(!f._opt){ + f._rx = /^([^=]+)=(.+)/; + f._rxInt = /^-?\d+$/; + f._opt = function(opt, rv){ + const m = f._rx.exec(opt); + rv[0] = (m ? m[1] : opt); + rv[1] = m ? (f._rxInt.test(m[2]) ? +m[2] : m[2]) : true; + }; + } + const rc = {}, ov = [0,0]; + let i = 0, k; + while((k = api.sqlite3_compileoption_get(i++))){ + f._opt(k,ov); + rc[ov[0]] = ov[1]; + } + return rc; + } + else if(Array.isArray(optName)){ + const rc = {}; + optName.forEach((v)=>{ + rc[v] = api.sqlite3_compileoption_used(v); + }); + return rc; + } + else if('object' === typeof optName){ + Object.keys(optName).forEach((k)=> { + optName[k] = api.sqlite3_compileoption_used(k); + }); + return optName; + } + return ( + 'string'===typeof optName + ) ? !!api.sqlite3_compileoption_used(optName) : false; + } + }; + + namespace.sqlite3 = { + api: api, + SQLite3 + }; +}); diff --git a/ext/fiddle/testing.css b/ext/fiddle/testing.css new file mode 100644 index 0000000000..f87dbd2cf1 --- /dev/null +++ b/ext/fiddle/testing.css @@ -0,0 +1,31 @@ +textarea { + font-family: monospace; +} +header { + font-size: 130%; + font-weight: bold; +} +.hidden, .initially-hidden { + position: absolute !important; + opacity: 0 !important; + pointer-events: none !important; + display: none !important; +} +fieldset.options { + font-size: 75%; +} +fieldset > legend { + padding: 0 0.5em; +} +span.labeled-input { + padding: 0.25em; + margin: 0.25em 0.5em; + border-radius: 0.25em; + white-space: nowrap; + background: #0002; +} +.center { text-align: center; } +.error { + color: red; + background-color: yellow; +} diff --git a/ext/fiddle/testing1.html b/ext/fiddle/testing1.html new file mode 100644 index 0000000000..bf22f30ff3 --- /dev/null +++ b/ext/fiddle/testing1.html @@ -0,0 +1,33 @@ + + + + + + + + + sqlite3-api.js tests + + + +
sqlite3-api.js tests
+ +
+
+
Initializing app...
+
+ On a slow internet connection this may take a moment. If this + message displays for "a long time", intialization may have + failed and the JavaScript console may contain clues as to why. +
+
+
Downloading...
+
+ +
+
Everything on this page happens in the dev console.
+ + + + + diff --git a/ext/fiddle/testing1.js b/ext/fiddle/testing1.js new file mode 100644 index 0000000000..9216db1341 --- /dev/null +++ b/ext/fiddle/testing1.js @@ -0,0 +1,155 @@ +/* + 2022-05-22 + + 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. + + *********************************************************************** + + A basic test script for sqlite3-api.js. +*/ +(function(){ + const T = self.SqliteTestUtil; + const log = console.log.bind(console); + + const assert = function(condition, text) { + if (!condition) { + throw new Error('Assertion failed' + (text ? ': ' + text : '')); + } + }; + + const test1 = function(db,sqlite3){ + const api = sqlite3.api; + log("Basic sanity tests..."); + T.assert(db._pDb); + let st = db.prepare("select 3 as a"); + //log("statement =",st); + T.assert(st._pStmt) + .assert(!st._mayGet) + .assert('a' === st.getColumnName(0)) + .assert(st === db._statements[st._pStmt]) + .assert(1===st.columnCount) + .assert(0===st.parameterCount) + .mustThrow(()=>st.bind(1,null)) + .assert(true===st.step()) + .assert(3 === st.get(0)) + .mustThrow(()=>st.get(1)) + .mustThrow(()=>st.get(0,~api.SQLITE_INTEGER)) + .assert(3 === st.get(0,api.SQLITE_INTEGER)) + .assert(3 === st.getInt(0)) + .assert('3' === st.get(0,api.SQLITE_TEXT)) + .assert('3' === st.getString(0)) + .assert(3.0 === st.get(0,api.SQLITE_FLOAT)) + .assert(3.0 === st.getFloat(0)) + .assert(st.get(0,api.SQLITE_BLOB) instanceof Uint8Array) + .assert(st.getBlob(0) instanceof Uint8Array) + .assert(3 === st.get([])[0]) + .assert(3 === st.get({}).a) + .assert(3 === st.getJSON(0)) + .assert(st._mayGet) + .assert(false===st.step()) + .assert(!st._mayGet) + ; + let pId = st._pStmt; + st.finalize(); + T.assert(!st._pStmt) + .assert(!db._statements[pId]); + + let list = []; + db.exec({ + sql:`CREATE TABLE t(a,b); +INSERT INTO t(a,b) VALUES(1,2),(3,4),(?,?);`, + multi: true, + saveSql: list, + bind: [5,6] + }); + T.assert(2 === list.length); + //log("Exec'd SQL:", list); + let counter = 0, colNames = []; + db.exec("SELECT a a, b b FROM t",{ + rowMode: 'object', + callback: function(row,stmt){ + if(!counter) stmt.getColumnNames(colNames); + ++counter; + T.assert(row.a%2 && row.a<6); + } + }); + assert(2 === colNames.length); + assert('a' === colNames[0]); + T.assert(3 === counter); + db.exec("SELECT a a, b b FROM t",{ + rowMode: 'array', + callback: function(row,stmt){ + ++counter; + assert(Array.isArray(row)); + T.assert(0===row[1]%2 && row[1]<7); + } + }); + T.assert(6 === counter); + }; + + const testUDF = function(db){ + log("Testing UDF..."); + db.createFunction("foo",function(a,b){return a+b}); + T.assert(7===db.selectValue("select foo(3,4)")). + assert(5===db.selectValue("select foo(3,?)",2)). + assert(5===db.selectValue("select foo(?,?)",[1,4])). + assert(5===db.selectValue("select foo($a,$b)",{$a:0,$b:5})); + db.createFunction("bar", { + arity: -1, + callback: function(){ + var rc = 0; + for(let i = 0; i < arguments.length; ++i) rc += arguments[i]; + return rc; + } + }); + + log("Testing DB::selectValue() w/ UDF..."); + T.assert(0===db.selectValue("select bar()")). + assert(1===db.selectValue("select bar(1)")). + assert(3===db.selectValue("select bar(1,2)")). + assert(-1===db.selectValue("select bar(1,2,-4)")); + + T.assert('hi' === db.selectValue("select ?",'hi')). + assert(null===db.selectValue("select null")). + assert(null === db.selectValue("select ?",null)). + assert(null === db.selectValue("select ?",[null])). + assert(null === db.selectValue("select $a",{$a:null})); + }; + + const runTests = function(Module){ + T.assert(Module._free instanceof Function). + assert(Module.allocate instanceof Function). + assert(Module.addFunction instanceof Function). + assert(Module.removeFunction instanceof Function); + const sqlite3 = Module.sqlite3; + const api = sqlite3.api; + const oo = sqlite3.SQLite3; + console.log("Loaded module:",api.sqlite3_libversion(), + api.sqlite3_sourceid()); + log("Build options:",oo.compileOptionUsed()); + const db = new oo.DB(); + try { + log("DB:",db.filename); + [ + test1, testUDF + ].forEach((f)=>f(db, sqlite3)); + }finally{ + db.close(); + } + log("Total Test count:",T.counter); + }; + + initSqlite3Module(self.sqlite3TestModule).then(function(theModule){ + /** Use a timeout so that we are (hopefully) out from + under the module init stack when our setup gets + run. Just on principle, not because we _need_ to + be. */ + //console.debug("theModule =",theModule); + setTimeout(()=>runTests(theModule), 0); + }); +})(); diff --git a/manifest b/manifest index 30e9e9f101..858e231283 100644 --- a/manifest +++ b/manifest @@ -1,9 +1,9 @@ -C Merge\srecent\strunk\senhancements\sinto\sthe\swal2\sbranch. -D 2022-05-10T12:39:00.740 +C Merge\sthe\slatest\strunk\senhancements\sinto\sthe\swal2\sbranch. +D 2022-05-28T14:44:19.777 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 -F Makefile.in d131e5632811fec684198fd7f579463d81b01048df48a55ec92584c538190f81 +F Makefile.in 6e2350cc3bed7a467fbdaa3ce42e665627aa91c9344c464cd21e750b75280181 F Makefile.linux-gcc f609543700659711fbd230eced1f01353117621dccae7b9fb70daa64236c5241 F Makefile.msc f969c28231012e50ee07c5062ee1dd5bbfc00883d52573af44b4fd477ec63843 F README.md 8b8df9ca852aeac4864eb1e400002633ee6db84065bd01b78c33817f97d31f5e @@ -56,6 +56,20 @@ F ext/expert/expert1.test 3c642a4e7bbb14f21ddab595436fb465a4733f47a0fe5b2855e1d5 F ext/expert/sqlite3expert.c 6ca30d73b9ed75bd56d6e0d7f2c962d2affaa72c505458619d0ff5d9cdfac204 F ext/expert/sqlite3expert.h ca81efc2679a92373a13a3e76a6138d0310e32be53d6c3bfaedabd158ea8969b F ext/expert/test_expert.c d56c194b769bdc90cf829a14c9ecbc1edca9c850b837a4d0b13be14095c32a72 +F ext/fiddle/EXPORTED_FUNCTIONS.fiddle 7fb73f7150ab79d83bb45a67d257553c905c78cd3d693101699243f36c5ae6c3 +F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api 540b9dec63a3a62a256e2f030827848a92e9b9d9b6fa5c0188295a4a1c5382cd +F ext/fiddle/EXPORTED_RUNTIME_METHODS b831017ba67ba993b34a27400cef2f6095bd6789c0fc4eba7e7a251c207be31c +F ext/fiddle/Makefile de65d04bfb312e94dbd7a0e7d99fb126f0abc1db62f920159c4124b5a42347d8 +F ext/fiddle/SqliteTestUtil.js 559731c3e8e0de330ec7d292e6c1846566408caee6637acc8a119ac338a8781c +F ext/fiddle/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f +F ext/fiddle/fiddle-worker.js 3a19253dc026d1ad9064ee853f3c4da3385223ce4434dab1838837525d817371 +F ext/fiddle/fiddle.html 724f1cd4126616bc87f5871f78d3f7aaaf41e45c9728724627baab87e6af35f0 +F ext/fiddle/fiddle.js 5b456ed7085830cda2fc75a0801476174a978521949335f24bc4154d076dcd4d +F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf +F ext/fiddle/sqlite3-api.js 8500698d2163f4a25f8e5e6810ad826487342579d6a321d82b244dbc8e6f6db6 +F ext/fiddle/testing.css 750572dded671d2cf142bbcb27af5542522ac08db128245d0b9fe410aa1d7f2a +F ext/fiddle/testing1.html ea1f3be727f78e420007f823912c1a03b337ecbb8e79449abc2244ad4fe15d9a +F ext/fiddle/testing1.js 94a7597955c8fdbd15839a70d9b8279bc690205dda65ff175f688f13bf315745 F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b F ext/fts1/ft_hash.h 06df7bba40dadd19597aa400a875dbc2fed705ea @@ -488,7 +502,7 @@ F spec.template 86a4a43b99ebb3e75e6b9a735d5fd293a24e90ca F sqlite.pc.in 42b7bf0d02e08b9e77734a47798d1a55a9e0716b F sqlite3.1 fc7ad8990fc8409983309bb80de8c811a7506786 F sqlite3.pc.in 48fed132e7cb71ab676105d2a4dc77127d8c1f3a -F src/alter.c d8872f9d1863d8e31c37475e318de746e1b5ca57c0e477e35042a9ebbb6e0298 +F src/alter.c 0390ca1d69ec3626cfa9f153114b7ab233e6b2bada6a9eb91361ed385fe90deb F src/analyze.c aabdf3769c7fd9954a8ec508eb7041ae174b66f88d12c47199fabbea9a646467 F src/attach.c 4431f82f0247bf3aaf91589acafdff77d1882235c95407b36da1585c765fbbc8 F src/auth.c f4fa91b6a90bbc8e0d0f738aa284551739c9543a367071f55574681e0f24f8cf @@ -506,10 +520,10 @@ F src/date.c 15082566229d4b1e5f24fdb490bf9bcc68824b911d70e3573ef075a1b9e2d26f F src/dbpage.c 90661a87e1db8bfbc8d2ebbdcd3749651ddb287c555c07a28fb17c7c591ffb68 F src/dbstat.c 861e08690fcb0f2ee1165eff0060ea8d4f3e2ea10f80dab7d32ad70443a6ff2d F src/delete.c a8e844af211a48b13b5b358be77a12c860c6a557c21990ad51a548e2536500ce -F src/expr.c d955e8954e03e637c19033241806b04f6a937d0657f6261f2d9e4cdfe674b478 +F src/expr.c 19507ae3244402860cac2944be3b92bf9a8b50212fbfabaf7e9817127fec7c00 F src/fault.c 460f3e55994363812d9d60844b2a6de88826e007 F src/fkey.c d965ede15d8360c09ed59348940649ee647b192e784466837d7aefa836d1d91e -F src/func.c a3407a6fbb0d4088d8d502e46f0ace63e0aeae7467ae23a9ca9815bbf9239761 +F src/func.c 41bf487f04d54e694baf84baacff7de621847fd7e89a35b776d5fb9ade772ff7 F src/global.c e83ee571b79ee3adc32e380cf554cf1254bc43763d23786c71721fbcdfbbb965 F src/hash.c 8d7dda241d0ebdafb6ffdeda3149a412d7df75102cecfc1021c98d6219823b19 F src/hash.h 3340ab6e1d13e725571d7cee6d3e3135f0779a7d8e76a9ce0a85971fa3953c51 @@ -518,8 +532,8 @@ F src/in-operator.md 10cd8f4bcd225a32518407c2fb2484089112fd71 F src/insert.c 173845e5a6bac96ae937409e4f876b631f26b31dabb9df8fd0eb3b130b2bb3a7 F src/json.c 7749b98c62f691697c7ee536b570c744c0583cab4a89200fdd0fc2aa8cc8cbd6 F src/legacy.c d7874bc885906868cd51e6c2156698f2754f02d9eee1bae2d687323c3ca8e5aa -F src/loadext.c 0705c2747212934183398f09891415d2f7f3113d0f543ccb205640210b20e617 -F src/main.c 135858d2ede0b83d779e71b07ede9c1d6b6eaab7b77bc2a85729584152769faf +F src/loadext.c 853385cc7a604157e137585097949252d5d0c731768e16b044608e5c95c3614b +F src/main.c e946f1d9e30bf49cf399ddf860cfd2319e3a97bbaabfb8e87d6ab985a20d54ad F src/malloc.c a9127efdcef92d6934c6339ea9813075b90edc0ce2e5c723556381a3828fb720 F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645 F src/mem1.c c12a42539b1ba105e3707d0e628ad70e611040d8f5e38cf942cee30c867083de @@ -536,31 +550,31 @@ F src/mutex_unix.c dd2b3f1cc1863079bc1349ac0fec395a500090c4fe4e11ab775310a49f2f9 F src/mutex_w32.c caa50e1c0258ac4443f52e00fe8aaea73b6d0728bd8856bedfff822cae418541 F src/notify.c 89a97dc854c3aa62ad5f384ef50c5a4a11d70fcc69f86de3e991573421130ed6 F src/os.c b1c4f2d485961e9a5b6b648c36687d25047c252222e9660b7cc25a6e1ea436ab -F src/os.h 26890f540b475598cd9881dcc68931377b8d429d3ea3e2eeb64470cde64199f8 +F src/os.h 1ff5ae51d339d0e30d8a9d814f4b8f8e448169304d83a7ed9db66a65732f3e63 F src/os_common.h b2f4707a603e36811d9b1a13278bffd757857b85 F src/os_setup.h 0dbaea40a7d36bf311613d31342e0b99e2536586 -F src/os_unix.c 1f71ec8c87621f75c9c5ea973f5e8ce2f1d23fe760c01ed2814fe4b98b639825 +F src/os_unix.c 2df2b33db88f00af13805d4573ee126bc5973f9e3b91d03c575fa7ba64e7dc41 F src/os_win.c a8ea80037e81127ca01959daa87387cc135f325c88dc745376c4f760de852a10 F src/os_win.h 7b073010f1451abe501be30d12f6bc599824944a F src/pager.c edd204a77e24583e4be80f8e0c596a5a2d941cafc618ac4c17f724893cfc704f F src/pager.h c49ff262186a78bc5f27e3891edefb900afa769b9e2eaeca0322c7f3553536d4 -F src/parse.y b86d56b446afb9c203d8354dc6c422818a62b4bbab52b76ab3da06d7b1d07e44 +F src/parse.y 8e67d820030d2655b9942ffe61c1e7e6b96cea2f2f72183533299393907d0564 F src/pcache.c 084e638432c610f95aea72b8509f0845d2791293f39d1b82f0c0a7e089c3bb6b F src/pcache.h 4f87acd914cef5016fae3030343540d75f5b85a1877eed1a2a19b9f284248586 F src/pcache1.c 54881292a9a5db202b2c0ac541c5e3ef9a5e8c4f1c1383adb2601d5499a60e65 F src/pragma.c 3aae3282d87f8301a916311a1e581b3194fe5b03372f509fb6cd94b9e8c6504d F src/pragma.h e690a356c18e98414d2e870ea791c1be1545a714ba623719deb63f7f226d8bb7 F src/prepare.c c62820c15dcb63013519c8e41d9f928d7478672cc902cfd0581c733c271dbf45 -F src/printf.c 512574910a45341c8ad244bd3d4939968ebdfde215645b676fff01cc46e90757 +F src/printf.c 6166a30417b05c5b2f82e1f183f75faa2926ad60531c0b688a57dbc951441a20 F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c -F src/resolve.c e9ee235c4151d2b7fa47435a219bfd30bf516a804d2f004639858087ebf3137b +F src/resolve.c a4eb3c617027fd049b07432f3b942ea7151fa793a332a11a7d0f58c9539e104f F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 -F src/select.c cd17de0cab436f0efc4cfeeeef1b8e03c41c40335ec757eff68e341e6a3f763f -F src/shell.c.in ae0a6fae983caac6f8c824733f0599dfdf7b3a7e8efdef3cb5e3ab2e457ffc35 -F src/sqlite.h.in 2a35f62185eb5e7ecc64a2f68442b538ce9be74f80f28a00abc24837edcf1c17 +F src/select.c 7a4c5023d6c3bcd243546dbe9bbf5b280a60ca565658d037b8c0ec8dd77e1136 +F src/shell.c.in b76e681f9e441928d574f21f9473ef615158bbeab1ae49f05ecab9d81730a51d +F src/sqlite.h.in 172528c287399a34f188154017b7268bf82c6d5b780902e361958d2318c4e37c F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 -F src/sqlite3ext.h f49e28c25bd941e79794db5415fdf7b202deb3bc072ed6f1ed273d578703684e -F src/sqliteInt.h 7c8146fc57f2e009152eb93ad41eab28490f9c31691559f281a80f9077ec1f2a +F src/sqlite3ext.h a988810c9b21c0dc36dc7a62735012339dc76fc7ab448fb0792721d30eacb69d +F src/sqliteInt.h 3064533677f135771e71843b5221482df18d6589afe65e6a7ef828ccb8879a5f F src/sqliteLimit.h d7323ffea5208c6af2734574bae933ca8ed2ab728083caa117c9738581a31657 F src/status.c 4a3da6d77eeb3531cb0dbdf7047772a2a1b99f98c69e90ce009c75fe6328b2c0 F src/table.c 0f141b58a16de7e2fbe81c308379e7279f4c6b50eb08efeec5892794a0ba30d1 @@ -568,7 +582,7 @@ F src/tclsqlite.c 1f6673991147bc2cecc08a40d22f9803b84c805b24b499fe727f392256f734 F src/test1.c 1356984e97bff07e4a8cc3863e892f05b3348678a74783bb6f350b76316736f1 F src/test2.c 3efb99ab7f1fc8d154933e02ae1378bac9637da5 F src/test3.c 61798bb0d38b915067a8c8e03f5a534b431181f802659a6616f9b4ff7d872644 -F src/test4.c 7c4420e01c577b5c4add2cb03119743b1a357543d347773b9e717195ea967159 +F src/test4.c 4533b76419e7feb41b40582554663ed3cd77aaa54e135cf76b3205098cd6e664 F src/test5.c 328aae2c010c57a9829d255dc099d6899311672d F src/test6.c ae73a3a42bbc982fb9e301b84d30bda65a307be48c6dff20aba1461e17a9b0ce F src/test7.c 5612e9aecf934d6df7bba6ce861fdf5ba5456010 @@ -620,8 +634,8 @@ F src/test_window.c cdae419fdcea5bad6dcd9368c685abdad6deb59e9fc8b84b153de513d394 F src/test_wsd.c 41cadfd9d97fe8e3e4e44f61a4a8ccd6f7ca8fe9 F src/threads.c 4ae07fa022a3dc7c5beb373cf744a85d3c5c6c3c F src/tokenize.c a38f52058b517929e264094abd0b5fd1e8e145a1aa43bc6f6a72ae5218f96c98 -F src/treeview.c 4153f2f044d13a3216e64271dfd1e59a3fa40222a45e6da248708088634e6f06 -F src/trigger.c 4fe4c1ac811755aff49d669d2e52e414eb5dfa6e172e849ab7b6825e70a571c0 +F src/treeview.c 73facf395c8841653b9a54e789d8c80e15bc3d0d1cb9d16104c2d889c15e33cd +F src/trigger.c 61bea163b1fa3039bc572ed8312461b978e5c527e5301f302b078f4c1ccdec6a F src/update.c 2cfaded82ca80ff56afb8c3ae5e88284e0824bfd86119827cc22481959f96f92 F src/upsert.c 8789047a8f0a601ea42fa0256d1ba3190c13746b6ba940fe2d25643a7e991937 F src/utf.c ee39565f0843775cc2c81135751ddd93eceb91a673ea2c57f61c76f288b041a0 @@ -631,7 +645,7 @@ F src/vdbe.c e2ce1c8c86bba823698b1c176283c8b06fbe19c2a1e7238822cefb4f6bce7e3e F src/vdbe.h 07641758ca8b4f4c6d81ea667ea167c541e6ece21f5574da11e3d21ec37e2662 F src/vdbeInt.h ef43f7fdc5fde29fc3fd29c506c12830f366178fdb4edbbf0cbc3dfbd1278b5f F src/vdbeapi.c 354c893f1500cf524cc45c32879b9c68893a28b77e3442c24668d6afe4236217 -F src/vdbeaux.c f406d8d8b461f260aeaa69d265c5d175ff0a9f84d6153c87975cdfbf9d681922 +F src/vdbeaux.c 75c4f75ed7e1d12eb3d80093a160ec998c839f3008a1c3c967fc5acf522d0e3c F src/vdbeblob.c 5e61ce31aca17db8fb60395407457a8c1c7fb471dde405e0cd675974611dcfcd F src/vdbemem.c 7189090b72baa025f945a1ac8c61ee420c645254476e8a191d555db76dfea5d4 F src/vdbesort.c 43756031ca7430f7aec3ef904824a7883c4ede783e51f280d99b9b65c0796e35 @@ -642,10 +656,10 @@ F src/vxworks.h d2988f4e5a61a4dfe82c6524dd3d6e4f2ce3cdb9 F src/wal.c a9cc35881efbb97e568b654dffa2716cf4c5d3fd15489980ab96e48fc5bc60a7 F src/wal.h d01234e828943e002040c22a7e017642962f9fd9b2dc142fa599769ae4e459e9 F src/walker.c f890a3298418d7cba3b69b8803594fdc484ea241206a8dfa99db6dd36f8cbb3b -F src/where.c aa585b89bd65a81e44bdfb871b55f65bf8fda88e1bc85efda6c236fe8d2bd788 -F src/whereInt.h 4db5a877a9d1f38b5c928c1c84297c07f30b9a3bc1f5f66214cf1a8ef90a0556 -F src/wherecode.c 72f8eeed5527450c8e2258160a7bd04534a76c161230d100da0f43a86c6e29ac -F src/whereexpr.c e036477ac8424de50ae5b36a71103405d3f86b33ba11125ec7a2a99d501b0622 +F src/where.c c4b64c6fa224e5b89ed547ec0ebdfd243c081509b195e71581164a2fbb8d4a80 +F src/whereInt.h 8da918f392bf202ccc0ee61291455b33ad171d209445f1ff3eaf62e0b6f6b363 +F src/wherecode.c 2a8a73bcf1886632f2b2247c79395f94852a4b74484d8aa70a005892ce73d339 +F src/whereexpr.c 7c5ee52e1df81d6a43f39e6b6f35d540fd37254e2b6e953a4e2715c3abf26f46 F src/window.c fff1b51757438c664e471d5184634e48dcdf8ea34b640f3b1b0810b1e06de18c F test/8_3_names.test ebbb5cd36741350040fd28b432ceadf495be25b2 F test/affinity2.test ce1aafc86e110685b324e9a763eab4f2a73f737842ec3b687bd965867de90627 @@ -668,11 +682,12 @@ F test/alterfault.test 289067108947bedca27534edd4ff251bcd298cf84402d7b24eaa37493 F test/alterlegacy.test f38c6d06cda39e1f7b955bbce57f2e3ef5b7cb566d3d1234502093e228c15811 F test/altermalloc.test 167a47de41b5c638f5f5c6efb59784002b196fff70f98d9b4ed3cd74a3fb80c9 F test/altermalloc2.test 17fb3724c4b004c469c27dc4ef181608aa644555fbd3f3236767584f73747c81 -F test/altermalloc3.test 55e606edf4b0acfbbd851ddfe93cfdddfae43d103644dcfd6008ae4ab3c44adf +F test/altermalloc3.test 8531b3086f0a7889f43971a579a8c81832d5123f4703d8c86b89ba1136c63b9a F test/alterqf.test ff6c6f881485c29ed699b8ef4774864ca1b0c01a6c08f5cdd624a008e4b40fca F test/altertab.test 7273b8506eab46342be016af78028df49f3bd99037412f997a8f1011b37a6912 F test/altertab2.test 62597b6fd08feaba1b6bfe7d31dac6117c67e06dc9ce9c478a3abe75b5926de0 -F test/altertab3.test 8af5c6eb4a7dd2fc73235b865b53561bf07428d1d6a9cd59a067abf51141891e +F test/altertab3.test 6c432fbb9963e0bd6549bf1422f6861d744ee5a80cb3298564e81e556481df16 +F test/altertrig.test fb5951d21a2c954be3b8a8cf8e10b5c0fa20687c53fd67d63cea88d08dd058d5 F test/amatch1.test b5ae7065f042b7f4c1c922933f4700add50cdb9f F test/analyze.test 547bb700f903107b38611b014ca645d6b5bb819f5210d7bf39c40802aafeb7d7 F test/analyze3.test 4440c4932247adb2b4e0c838f657c19dc7af4f56859255436dc4e855f39b9324 @@ -881,7 +896,7 @@ F test/e_fts3.test 17ba7c373aba4d4f5696ba147ee23fd1a1ef70782af050e03e262ca187c5e F test/e_insert.test f02f7f17852b2163732c6611d193f84fc67bc641fb4882c77a464076e5eba80e F test/e_reindex.test 2b0e29344497d9a8a999453a003cb476b6b1d2eef2d6c120f83c2d3a429f3164 F test/e_resolve.test a61751c368b109db73df0f20fc75fb47e166b1d8 -F test/e_select.test 9b7ca08975c5444844b35ee60e09f973787a9f3317719715e8e6669abdf6aba2 +F test/e_select.test 89fa483f68d868f1be3d6f56180ed42d979979c266e051bf8b5e66a251e6b44a F test/e_select2.test aceb80ab927d46fba5ce7586ebabf23e2bb0604f F test/e_totalchanges.test c927f7499dc3aa28b9b556b7d6d115a2f0fe41f012b128d16bf1f3b30e9b41e4 F test/e_update.test f46c2554d915c9197548681e8d8c33a267e84528 @@ -903,7 +918,7 @@ F test/exclusive.test 7ff63be7503990921838d5c9f77f6e33e68e48ed1a9d48cd28745bf650 F test/exclusive2.test 984090e8e9d1b331d2e8111daf6e5d61dda0bef7 F test/exec.test e949714dc127eaa5ecc7d723efec1ec27118fdd7 F test/exists.test 79a75323c78f02bbe9c251ea502a092f9ef63dac -F test/expr.test e1afcdb1038e4d3fa67a3df323347c38750946e2e1b4e385bdc75d26284f2dac +F test/expr.test 5c06696478212e5a04e04b043f993373f6f8e5ce5a80f5548a84703b123b6caa F test/expr2.test c27327ae9c017a7ff6280123f67aff496f912da74d78c888926d68b46ec75fd8 F test/exprfault.test 497cc0b8fe6a677f49b55cb485e040f709ec2834b84f25912fe9c2dfeeda33db F test/extension01.test 00d13cec817f331a687a243e0e5a2d87b0e358c9 @@ -1060,7 +1075,7 @@ F test/func3.test 600a632c305a88f3946d38f9a51efe145c989b2e13bd2b2a488db47fe76bab F test/func4.test 2285fb5792d593fef442358763f0fd9de806eda47dbc7a5934df57ffdc484c31 F test/func5.test 863e6d1bd0013d09c17236f8a13ea34008dd857d87d85a13a673960e4c25d82a F test/func6.test 9cc9b1f43b435af34fe1416eb1e318c8920448ea7a6962f2121972f5215cb9b0 -F test/func7.test b9e2a1a30a8562b00841b4a21a5d2d81754fa3ab99275fd71fd5279287b44b1c +F test/func7.test adbfc910385a6ffd15dc47be3c619ef070c542fcb7488964badb17b2d9a4d080 F test/fuzz-oss1.test 514dcabb24687818ea949fa6760229eaacad74ca70157743ef36d35bbe01ffb0 F test/fuzz.test 4608c1310cff4c3014a84bcced6278139743e080046e5f6784b0de7b069371d8 F test/fuzz2.test 76dc35b32b6d6f965259508508abce75a6c4d7e1 @@ -1091,7 +1106,7 @@ F test/hook2.test b9ff3b8c6519fb67f33192f1afe86e7782ee4ac8 F test/icu.test 716a6b89fbabe5cc63e0cd4c260befb08fd7b9d761f04d43669233292f0753b1 F test/ieee754.test b0945d12be7d255f3dfa18e2511b17ca37e0edd2b803231c52d05b86c04ab26e F test/imposter1.test c3f1db2d3db2c24611a6596a3fc0ffc14f1466c8 -F test/in.test 15de58ee017f43d36390812e9a51217d1b2db7758f97d0df48296ef178ea560b +F test/in.test 55503d3633c434120d8b2d5966a34f0d9a55393a589050b1366afe4b188093c7 F test/in2.test 5d4c61d17493c832f7d2d32bef785119e87bde75 F test/in3.test 3cbf58c87f4052cee3a58b37b6389777505aa0c0 F test/in4.test fdd1d8134da8376985c2edba6035a2de1f6c731524d2ffa651419e8fe2cd1c5a @@ -1143,19 +1158,20 @@ F test/ioerr4.test f130fe9e71008577b342b8874d52984bd04ede2c F test/ioerr5.test 2edfa4fb0f896f733071303b42224df8bedd9da4 F test/ioerr6.test a395a6ab144b26a9e3e21059a1ab6a7149cca65b F test/istrue.test e7f285bb70282625c258e866ce6337d4c762922f5a300e1b50f958aef6e7d9c9 -F test/join.test e5f165dfd84fd46406ddae6614b0122c3bfa23a26ef62966442e1503c40d96aa +F test/join.test edeaff6edc1c1a2bcfebee343744e04d000f861c3d67cb653114f88565f8c955 F test/join2.test 466b07233820f5deee66a6c3bf6e4500c8bbf7b83649e67606f5f649c07928c0 F test/join3.test 6f0c774ff1ba0489e6c88a3e77b9d3528fb4fda0 F test/join4.test 1a352e4e267114444c29266ce79e941af5885916 F test/join5.test d22b6cba8fb59ab3f1c82701434c360705eb12d4ce200c449f37b018fc47681a F test/join6.test f809c025fa253f9e150c0e9afd4cef8813257bceeb6f46e04041228c9403cc2c F test/join7.test 8e72de4b45e5e930d18c305c7efe86015fb2552731e4e03ea226353036b0dab0 -F test/join8.test 68f5ec206cd88610c19ab8edb4789a174a55cdb1732619a95db8fd33dbb13783 +F test/join8.test 616eb7c2e4f2a54f2d730b914884d2205c8ada4757e89d08089255964a28e78e F test/join9.test 9056ddd3b0c0f4f9d658f4521038d9a37dc23ead8ca9a505d0b0db2b6a471e05 F test/joinA.test 7eab225dc1c1ab258a5e62513a4ed7cabbd3db971d59d5d92f4fb6fa14c12f6a F test/joinB.test 1b2ba3fc8568b49411787fccbf540570c148e9b6a53a30f80691cb6268098ded F test/joinC.test 1f1a602c2127f55f136e2cbd3bf2d26546614bf8cffe5902ec1ac9c07f87f207 F test/joinD.test 7f0f4dd1f2767330bf1fda5c9cc8a437015a54bcd2355036b4d04ddfc1519d76 +F test/joinE.test d5d182f3812771e2c0d97c9dcf5dbe4c41c8e21c82560e59358731c4a3981d6b F test/journal1.test c7b768041b7f494471531e17abc2f4f5ebf9e5096984f43ed17c4eb80ba34497 F test/journal2.test 9dac6b4ba0ca79c3b21446bbae993a462c2397c4 F test/journal3.test 7c3cf23ffc77db06601c1fcfc9743de8441cb77db9d1aa931863d94f5ffa140e @@ -1296,7 +1312,7 @@ F test/parser1.test 6ccdf5e459a5dc4673d3273dc311a7e9742ca952dd0551a6a6320d27035c F test/pcache.test c8acbedd3b6fd0f9a7ca887a83b11d24a007972b F test/pcache2.test af7f3deb1a819f77a6d0d81534e97d1cf62cd442 F test/percentile.test 4243af26b8f3f4555abe166f723715a1f74c77ff -F test/permutations.test bceeee7ff77e371ce105773dda13111333cdaeb9c72311751febf39ac5b313a6 +F test/permutations.test d2de4f399b1ffe3305b7e1faf87dd1d7098089d502b5074b5415b9519406ff41 F test/pg_common.tcl 3b27542224db1e713ae387459b5d117c836a5f6e328846922993b6d2b7640d9f F test/pragma.test cae534c12a033a5c319ccc94f50b32811acdef9f67bf19a82ff42697caccd69f F test/pragma2.test e5d5c176360c321344249354c0c16aec46214c9f @@ -1399,14 +1415,14 @@ F test/sharedA.test 49d87ec54ab640fbbc3786ee3c01de94aaa482a3a9f834ad3fe92770eb69 F test/sharedB.test 16cc7178e20965d75278f410943109b77b2e645e F test/shared_err.test 32634e404a3317eeb94abc7a099c556a346fdb8fb3858dbe222a4cbb8926a939 F test/sharedlock.test 5ede3c37439067c43b0198f580fd374ebf15d304 -F test/shell1.test f7a2ef8260aa01f20be3185118213b1ae70518fdcd2105f3e25b021b5ca800ac -F test/shell2.test 7a3a23a9f57b99453f1679b1fe8072cb30e382a622874c0c4d97695fadb0a787 -F test/shell3.test a50628ab1d78d90889d9d3f32fb2c084ee15674771e96afe954aaa0accd1de3c -F test/shell4.test 8f6c0fce4abed19a8a7f7262517149812a04caa905d01bdc8f5e92573504b759 -F test/shell5.test 78a7a8516b1e7de560748881424f621321549023d3e5f7ed2e1c56497f64c06c +F test/shell1.test e4b4de56f454708e0747b52915135baa2cbfec4965406d6eaf02a4a5c22a9880 +F test/shell2.test c536c2aab4852608f8a606262330797abc4d964a4c2c782a7760f54ea1f17a6a +F test/shell3.test 91febeac0412812bf6370abb8ed72700e32bf8f9878849414518f662dfd55e8a +F test/shell4.test 7dc8a515705bc093d8ffe381670e8fa7a969661e8ed177c35c847e3c6dfc35e2 +F test/shell5.test c8b6c54f26ec537f8558273d7ed293ca3725ef42e6b12b8f151718628bd1473b F test/shell6.test 1ceb51b2678c472ba6cf1e5da96679ce8347889fe2c3bf93a0e0fa73f00b00d3 F test/shell7.test 115132f66d0463417f408562cc2cf534f6bbc6d83a6d50f0072a9eb171bae97f -F test/shell8.test 388471d16e4de767333107e30653983f186232c0e863f4490bb230419e830aae +F test/shell8.test 3fd093d481aaa94dc77fb73f1044c1f19c7efe3477a395cc4f7450133bc54915 F test/shmlock.test 3dbf017d34ab0c60abe6a44e447d3552154bd0c87b41eaf5ceacd408dd13fda5 F test/shortread1.test bb591ef20f0fd9ed26d0d12e80eee6d7ac8897a3 F test/show_speedtest1_rtree.tcl 32e6c5f073d7426148a6936a0408f4b5b169aba5 @@ -1464,7 +1480,7 @@ F test/subtype1.test 7fe09496352f97053af1437150751be2d0a0cae8 F test/superlock.test ec94f0556b6488d97f71c79f9061ae08d9ab8f12 F test/swarmvtab.test 250231404fcac88f61a6c147bb0e3a118ed879278cd3ccb0ae2d3a729e1e8e26 F test/swarmvtab2.test c948cb2fdfc5b01d85e8f6d6504854202dc1a0782ab2a0ed61538f27cbd0aa5c -F test/swarmvtab3.test 247aa38b6ebd2b99db2075847ae47e789ac34f1c2ab5c720dfcffd990004c544 +F test/swarmvtab3.test 41a3ab47cb7a834d4e5336425103b617410a67bb95d335ef536f887587ece073 F test/swarmvtabfault.test 8a67a9f27c61073a47990829e92bc0c64420a807cb642b15a25f6c788210ed95 F test/symlink.test 72b22238d4405ba34df8e60b335d290a3b1129fd5c260835c944c1e4e77288a9 F test/symlink2.test 9531f475a53d8781c4f81373f87faf2e2aff4f5fb2102ec6386e0c827916a670 @@ -1484,7 +1500,7 @@ F test/temptable.test d2c9b87a54147161bcd1822e30c1d1cd891e5b30 F test/temptable2.test d2940417496e2b9548e01d09990763fbe88c316504033256d51493e1f1a5ce6a F test/temptable3.test d11a0974e52b347e45ee54ef1923c91ed91e4637 F test/temptrigger.test 38f0ca479b1822d3117069e014daabcaacefffcc -F test/tester.tcl 079d70991d583b6513614c5b4656fa598c118373f66ce25736801778916bd7e1 +F test/tester.tcl 1942c73d4c23ee67149263e67a0c72db5d900cd2073b65704f5c61bd2ed51cc0 F test/thread001.test b61a29dd87cf669f5f6ac96124a7c97d71b0c80d9012746072055877055cf9ef F test/thread002.test e630504f8a06c00bf8bbe68528774dd96aeb2e58 F test/thread003.test ee4c9efc3b86a6a2767516a37bd64251272560a7 @@ -1693,6 +1709,7 @@ F test/upfrom1.tcl 8859d9d437f03b44174c4524a7a734a391fd4526fcff65be08285dafc9dc9 F test/upfrom1.test 8cb06689e99cd707d884faa16da0e8eb26ff658bb01c47ddf72fadade666e6e1 F test/upfrom2.test 66f3ebf721b3cebd922faee5c386bf244f816d416b57c000753ff51af62328a1 F test/upfrom3.test 6130f24ebf97f5ea865e5d2a14a2d543fe5428a62e87cc60f62d875e45c1f5f0 +F test/upfrom4.test 1cd82e9423e02b1f63d069e8665c6c3932ec424fd0043d033cc0ba99abf33236 F test/upfromfault.test 3a10075a0043f0c4fad6614b2c371f88a8ba5a4acab68b907438413865d6a8d6 F test/upsert1.test b0ae2f58680c5205b4bc1cdeed3c3d444057c506f6c44494fa3eac60731d68a2 F test/upsert2.test 720e94d09f7362a282bc69b3c6b83d51daeaaf0440eb4920a08b86518b8c7496 @@ -1721,7 +1738,7 @@ F test/vtab2.test 14d4ab26cee13ba6cf5c5601b158e4f57552d3b055cdd9406cf7f711e9c840 F test/vtab3.test b45f47d20f225ccc9c28dc915d92740c2dee311e F test/vtab4.test 8e73ed268f3d596bc3590f45fc948fb40f28e9c3 F test/vtab5.test 889f444970393c73f1e077e2bdc5d845e157a391 -F test/vtab6.test fa609a4af96da30beceefa3cb624abe9be38c4747ab373d98179b24027d6b798 +F test/vtab6.test 2525a2fe2e44ccbed1d758cb2977fb8ab8f07d5312ed8d8799d3529647d11f2f F test/vtab7.test 70c6f4a1d6177144a8236e4172d5fba92e683440374664ad1f04851fbb335d3c F test/vtab8.test e19fa4a538fcd1bb66c22825fa8f71618fb13583 F test/vtab9.test ea58d2b95d61955f87226381716b2d0b1d4e4f9b @@ -1790,7 +1807,7 @@ F test/walthread.test 14b20fcfa6ae152f5d8e12f5dc8a8a724b7ef189f5d8ef1e2ceab79f2a F test/walvfs.test bccb3e0d235ef85e276f491d34db32c9ada1ea67be8d9f10aabe7b30319ec656 F test/wapp.tcl b440cd8cf57953d3a49e7ee81e6a18f18efdaf113b69f7d8482b0710a64566ec F test/wapptest.tcl 899594e25684861d5b0c0880fb012364def50ef8097041b8ddf74be5ba7fa270 x -F test/where.test 8c6bbd0cae8feae142a7946e3484a802fa566bacf38452b1c3e48cb77321f9a4 +F test/where.test d13cd7c24e80009d2b54e2f7a8893c457afa49c64f99359c9eb3fe668ba1d9d4 F test/where2.test 03c21a11e7b90e2845fc3c8b4002fc44cc2797fa74c86ee47d70bd7ea4f29ed6 F test/where3.test 5b4ffc0ac2ea0fe92f02b1244b7531522fe4d7bccf6fa8741d54e82c10e67753 F test/where4.test 4a371bfcc607f41d233701bdec33ac2972908ba8 @@ -1966,8 +1983,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P bafaefc6ab4fa59c0a3859cd16a9a29d3d157953fd9002c96e868fc7a54fa18c c6c3115f3a008cf9b0d7c5c812f17e38c8a75a904032c5f05f0bea03a7340527 -R 118560a4a0b88970fe1255a6a6c64d6a +P 33d77fea4084c5aba9203dfeddb820424f102dcb8347dc59e32b922bdb241382 7e87892c249f023ee9ed1d5f75a9ad8db10fb38f14dd9e6954b12b9b28400b07 +R c9081974dfbb7d140ac326c1bbce54a8 U drh -Z 403140c147b4e9650876f7b5434cd478 +Z bbe2f38f083ae616992549cc7e11561e # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 8392dc912b..1d190a1cb4 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -33d77fea4084c5aba9203dfeddb820424f102dcb8347dc59e32b922bdb241382 \ No newline at end of file +934656f13dabc41ccf307b10dca7377c758b8a3b93eca57c072745c2786d6b3c \ No newline at end of file diff --git a/src/alter.c b/src/alter.c index 538ba7d9f8..d8a9982873 100644 --- a/src/alter.c +++ b/src/alter.c @@ -1317,27 +1317,33 @@ static int renameResolveTrigger(Parse *pParse){ if( rc==SQLITE_OK && pStep->zTarget ){ SrcList *pSrc = sqlite3TriggerStepSrc(pParse, pStep); if( pSrc ){ - int i; - for(i=0; inSrc && rc==SQLITE_OK; i++){ - SrcItem *p = &pSrc->a[i]; - p->iCursor = pParse->nTab++; - if( p->pSelect ){ - sqlite3SelectPrep(pParse, p->pSelect, 0); - sqlite3ExpandSubquery(pParse, p); - assert( i>0 ); - assert( pStep->pFrom->a[i-1].pSelect ); - sqlite3SelectPrep(pParse, pStep->pFrom->a[i-1].pSelect, 0); - }else{ - p->pTab = sqlite3LocateTableItem(pParse, 0, p); - if( p->pTab==0 ){ - rc = SQLITE_ERROR; - }else{ - p->pTab->nTabRef++; - rc = sqlite3ViewGetColumnNames(pParse, p->pTab); + Select *pSel = sqlite3SelectNew( + pParse, pStep->pExprList, pSrc, 0, 0, 0, 0, 0, 0 + ); + if( pSel==0 ){ + pStep->pExprList = 0; + pSrc = 0; + rc = SQLITE_NOMEM; + }else{ + sqlite3SelectPrep(pParse, pSel, 0); + rc = pParse->nErr ? SQLITE_ERROR : SQLITE_OK; + assert( pStep->pExprList==0 || pStep->pExprList==pSel->pEList ); + assert( pSrc==pSel->pSrc ); + if( pStep->pExprList ) pSel->pEList = 0; + pSel->pSrc = 0; + sqlite3SelectDelete(db, pSel); + } + if( pStep->pFrom ){ + int i; + for(i=0; ipFrom->nSrc && rc==SQLITE_OK; i++){ + SrcItem *p = &pStep->pFrom->a[i]; + if( p->pSelect ){ + sqlite3SelectPrep(pParse, p->pSelect, 0); } } } - if( rc==SQLITE_OK && db->mallocFailed ){ + + if( db->mallocFailed ){ rc = SQLITE_NOMEM; } sNC.pSrcList = pSrc; @@ -1789,6 +1795,15 @@ static void renameTableFunc( if( pStep->zTarget && 0==sqlite3_stricmp(pStep->zTarget, zOld) ){ renameTokenFind(&sParse, &sCtx, pStep->zTarget); } + if( pStep->pFrom ){ + int i; + for(i=0; ipFrom->nSrc; i++){ + SrcItem *pItem = &pStep->pFrom->a[i]; + if( 0==sqlite3_stricmp(pItem->zName, zOld) ){ + renameTokenFind(&sParse, &sCtx, pItem->zName); + } + } + } } } } diff --git a/src/expr.c b/src/expr.c index 1ebc73be93..93b23049df 100644 --- a/src/expr.c +++ b/src/expr.c @@ -1069,6 +1069,7 @@ Expr *sqlite3ExprFunction( sqlite3ExprListDelete(db, pList); /* Avoid memory leak when malloc fails */ return 0; } + assert( !ExprHasProperty(pNew, EP_InnerON|EP_OuterON) ); pNew->w.iOfst = (int)(pToken->z - pParse->zTail); if( pList && pList->nExpr > pParse->db->aLimit[SQLITE_LIMIT_FUNCTION_ARG] @@ -1347,7 +1348,7 @@ static int dupedExprStructSize(const Expr *p, int flags){ nSize = EXPR_FULLSIZE; }else{ assert( !ExprHasProperty(p, EP_TokenOnly|EP_Reduced) ); - assert( !ExprHasProperty(p, EP_FromJoin) ); + assert( !ExprHasProperty(p, EP_OuterON) ); assert( !ExprHasProperty(p, EP_MemToken) ); assert( !ExprHasVVAProperty(p, EP_NoReduce) ); if( p->pLeft || p->x.pList ){ @@ -2173,7 +2174,7 @@ static int exprNodeIsConstant(Walker *pWalker, Expr *pExpr){ /* If pWalker->eCode is 2 then any term of the expression that comes from ** the ON or USING clauses of an outer join disqualifies the expression ** from being considered constant. */ - if( pWalker->eCode==2 && ExprHasProperty(pExpr, EP_FromJoin) ){ + if( pWalker->eCode==2 && ExprHasProperty(pExpr, EP_OuterON) ){ pWalker->eCode = 0; return WRC_Abort; } @@ -2320,10 +2321,10 @@ int sqlite3ExprIsTableConstraint(Expr *pExpr, const SrcItem *pSrc){ return 0; /* rule (3) */ } if( pSrc->fg.jointype & JT_LEFT ){ - if( !ExprHasProperty(pExpr, EP_FromJoin) ) return 0; /* rule (4a) */ + if( !ExprHasProperty(pExpr, EP_OuterON) ) return 0; /* rule (4a) */ if( pExpr->w.iJoin!=pSrc->iCursor ) return 0; /* rule (4b) */ }else{ - if( ExprHasProperty(pExpr, EP_FromJoin) ) return 0; /* rule (5) */ + if( ExprHasProperty(pExpr, EP_OuterON) ) return 0; /* rule (5) */ } return sqlite3ExprIsTableConstant(pExpr, pSrc->iCursor); /* rules (1), (2) */ } @@ -2745,11 +2746,7 @@ int sqlite3FindInIndex( assert( pX->op==TK_IN ); mustBeUnique = (inFlags & IN_INDEX_LOOP)!=0; - if( pX->iTable && (inFlags & IN_INDEX_REUSE_CUR)!=0 ){ - iTab = pX->iTable; - }else{ - iTab = pParse->nTab++; - } + iTab = pParse->nTab++; /* If the RHS of this IN(...) operator is a SELECT, and if it matters ** whether or not the SELECT result contains NULL values, check whether @@ -3083,9 +3080,8 @@ void sqlite3CodeRhsOfIN( assert( ExprUseYSub(pExpr) ); sqlite3VdbeAddOp2(v, OP_Gosub, pExpr->y.sub.regReturn, pExpr->y.sub.iAddr); - if( iTab!=pExpr->iTable ){ - sqlite3VdbeAddOp2(v, OP_OpenDup, iTab, pExpr->iTable); - } + assert( iTab!=pExpr->iTable ); + sqlite3VdbeAddOp2(v, OP_OpenDup, iTab, pExpr->iTable); sqlite3VdbeJumpHere(v, addrOnce); return; } @@ -5078,8 +5074,8 @@ static void exprCodeBetween( ** so that the sqlite3ExprCodeTarget() routine will not attempt to move ** it into the Parse.pConstExpr list. We should use a new bit for this, ** for clarity, but we are out of bits in the Expr.flags field so we - ** have to reuse the EP_FromJoin bit. Bummer. */ - pDel->flags |= EP_FromJoin; + ** have to reuse the EP_OuterON bit. Bummer. */ + pDel->flags |= EP_OuterON; sqlite3ExprCodeTarget(pParse, &exprAnd, dest); } sqlite3ReleaseTempReg(pParse, regFree1); @@ -5764,7 +5760,7 @@ int sqlite3ExprImpliesExpr( static int impliesNotNullRow(Walker *pWalker, Expr *pExpr){ testcase( pExpr->op==TK_AGG_COLUMN ); testcase( pExpr->op==TK_AGG_FUNCTION ); - if( ExprHasProperty(pExpr, EP_FromJoin) ) return WRC_Prune; + if( ExprHasProperty(pExpr, EP_OuterON) ) return WRC_Prune; switch( pExpr->op ){ case TK_ISNOT: case TK_ISNULL: @@ -5861,7 +5857,7 @@ static int impliesNotNullRow(Walker *pWalker, Expr *pExpr){ ** False positives are not allowed, however. A false positive may result ** in an incorrect answer. ** -** Terms of p that are marked with EP_FromJoin (and hence that come from +** Terms of p that are marked with EP_OuterON (and hence that come from ** the ON or USING clauses of OUTER JOINS) are excluded from the analysis. ** ** This routine is used to check if a LEFT JOIN can be converted into diff --git a/src/func.c b/src/func.c index 7ccb5118e8..6c831c74bc 100644 --- a/src/func.c +++ b/src/func.c @@ -2098,11 +2098,11 @@ static void logFunc( switch( SQLITE_PTR_TO_INT(sqlite3_user_data(context)) ){ case 1: /* Convert from natural logarithm to log base 10 */ - ans *= 1.0/M_LN10; + ans /= M_LN10; break; case 2: /* Convert from natural logarithm to log base 2 */ - ans *= 1.0/M_LN2; + ans /= M_LN2; break; default: break; diff --git a/src/loadext.c b/src/loadext.c index bba431096d..dd15d9a4da 100644 --- a/src/loadext.c +++ b/src/loadext.c @@ -503,11 +503,12 @@ static const sqlite3_api_routines sqlite3Apis = { /* Version 3.39.0 and later */ #ifndef SQLITE_OMIT_DESERIALIZE sqlite3_deserialize, - sqlite3_serialize + sqlite3_serialize, #else 0, - 0 + 0, #endif + sqlite3_db_name }; /* True if x is the directory separator character diff --git a/src/main.c b/src/main.c index c40b3162ab..3ae1f4fe30 100644 --- a/src/main.c +++ b/src/main.c @@ -4626,6 +4626,24 @@ Btree *sqlite3DbNameToBtree(sqlite3 *db, const char *zDbName){ return iDb<0 ? 0 : db->aDb[iDb].pBt; } +/* +** Return the name of the N-th database schema. Return NULL if N is out +** of range. +*/ +const char *sqlite3_db_name(sqlite3 *db, int N){ +#ifdef SQLITE_ENABLE_API_ARMOR + if( !sqlite3SafetyCheckOk(db) ){ + (void)SQLITE_MISUSE_BKPT; + return 0; + } +#endif + if( N<0 || N>=db->nDb ){ + return 0; + }else{ + return db->aDb[N].zDbSName; + } +} + /* ** Return the filename of the database associated with a database ** connection. diff --git a/src/os.h b/src/os.h index 1a8eff3298..aeb01721c2 100644 --- a/src/os.h +++ b/src/os.h @@ -39,6 +39,13 @@ # define SQLITE_MAX_PATHLEN FILENAME_MAX #endif +/* Maximum number of symlinks that will be resolved while trying to +** expand a filename in xFullPathname() in the VFS. +*/ +#ifndef SQLITE_MAX_SYMLINK +# define SQLITE_MAX_SYMLINK 200 +#endif + /* ** The default size of a disk sector */ diff --git a/src/os_unix.c b/src/os_unix.c index 03ac3e46c9..b933de3765 100644 --- a/src/os_unix.c +++ b/src/os_unix.c @@ -6423,86 +6423,99 @@ static int unixAccess( } /* -** If the last component of the pathname in z[0]..z[j-1] is something -** other than ".." then back it out and return true. If the last -** component is empty or if it is ".." then return false. +** A pathname under construction */ -static int unixBackupDir(const char *z, int *pJ){ - int j = *pJ; - int i; - if( j<=0 ) return 0; - for(i=j-1; i>0 && z[i-1]!='/'; i--){} - if( i==0 ) return 0; - if( z[i]=='.' && i==j-2 && z[i+1]=='.' ) return 0; - *pJ = i-1; - return 1; +typedef struct DbPath DbPath; +struct DbPath { + int rc; /* Non-zero following any error */ + int nSymlink; /* Number of symlinks resolved */ + char *zOut; /* Write the pathname here */ + int nOut; /* Bytes of space available to zOut[] */ + int nUsed; /* Bytes of zOut[] currently being used */ +}; + +/* Forward reference */ +static void appendAllPathElements(DbPath*,const char*); + +/* +** Append a single path element to the DbPath under construction +*/ +static void appendOnePathElement( + DbPath *pPath, /* Path under construction, to which to append zName */ + const char *zName, /* Name to append to pPath. Not zero-terminated */ + int nName /* Number of significant bytes in zName */ +){ + assert( nName>0 ); + assert( zName!=0 ); + if( zName[0]=='.' ){ + if( nName==1 ) return; + if( zName[1]=='.' && nName==2 ){ + if( pPath->nUsed<=1 ){ + pPath->rc = SQLITE_ERROR; + return; + } + assert( pPath->zOut[0]=='/' ); + while( pPath->zOut[--pPath->nUsed]!='/' ){} + return; + } + } + if( pPath->nUsed + nName + 2 >= pPath->nOut ){ + pPath->rc = SQLITE_ERROR; + return; + } + pPath->zOut[pPath->nUsed++] = '/'; + memcpy(&pPath->zOut[pPath->nUsed], zName, nName); + pPath->nUsed += nName; +#if defined(HAVE_READLINK) && defined(HAVE_LSTAT) + if( pPath->rc==SQLITE_OK ){ + const char *zIn; + struct stat buf; + pPath->zOut[pPath->nUsed] = 0; + zIn = pPath->zOut; + if( osLstat(zIn, &buf)!=0 ){ + if( errno!=ENOENT ){ + pPath->rc = unixLogError(SQLITE_CANTOPEN_BKPT, "lstat", zIn); + } + }else if( S_ISLNK(buf.st_mode) ){ + ssize_t got; + char zLnk[SQLITE_MAX_PATHLEN+2]; + if( pPath->nSymlink++ > SQLITE_MAX_SYMLINK ){ + pPath->rc = SQLITE_CANTOPEN_BKPT; + return; + } + got = osReadlink(zIn, zLnk, sizeof(zLnk)-2); + if( got<=0 || got>=(ssize_t)sizeof(zLnk)-2 ){ + pPath->rc = unixLogError(SQLITE_CANTOPEN_BKPT, "readlink", zIn); + return; + } + zLnk[got] = 0; + if( zLnk[0]=='/' ){ + pPath->nUsed = 0; + }else{ + pPath->nUsed -= nName + 1; + } + appendAllPathElements(pPath, zLnk); + } + } +#endif } /* -** Convert a relative pathname into a full pathname. Also -** simplify the pathname as follows: -** -** Remove all instances of /./ -** Remove all isntances of /X/../ for any X +** Append all path elements in zPath to the DbPath under construction. */ -static int mkFullPathname( - const char *zPath, /* Input path */ - char *zOut, /* Output buffer */ - int nOut /* Allocated size of buffer zOut */ +static void appendAllPathElements( + DbPath *pPath, /* Path under construction, to which to append zName */ + const char *zPath /* Path to append to pPath. Is zero-terminated */ ){ - int nPath = sqlite3Strlen30(zPath); - int iOff = 0; - int i, j; - if( zPath[0]!='/' ){ - if( osGetcwd(zOut, nOut-2)==0 ){ - return unixLogError(SQLITE_CANTOPEN_BKPT, "getcwd", zPath); + int i = 0; + int j = 0; + do{ + while( zPath[i] && zPath[i]!='/' ){ i++; } + if( i>j ){ + appendOnePathElement(pPath, &zPath[j], i-j); } - iOff = sqlite3Strlen30(zOut); - zOut[iOff++] = '/'; - } - if( (iOff+nPath+1)>nOut ){ - /* SQLite assumes that xFullPathname() nul-terminates the output buffer - ** even if it returns an error. */ - zOut[iOff] = '\0'; - return SQLITE_CANTOPEN_BKPT; - } - sqlite3_snprintf(nOut-iOff, &zOut[iOff], "%s", zPath); - - /* Remove duplicate '/' characters. Except, two // at the beginning - ** of a pathname is allowed since this is important on windows. */ - for(i=j=1; zOut[i]; i++){ - zOut[j++] = zOut[i]; - while( zOut[i]=='/' && zOut[i+1]=='/' ) i++; - } - zOut[j] = 0; - - assert( zOut[0]=='/' ); - for(i=j=0; zOut[i]; i++){ - if( zOut[i]=='/' ){ - /* Skip over internal "/." directory components */ - if( zOut[i+1]=='.' && zOut[i+2]=='/' ){ - i += 1; - continue; - } - - /* If this is a "/.." directory component then back out the - ** previous term of the directory if it is something other than "..". - */ - if( zOut[i+1]=='.' - && zOut[i+2]=='.' - && zOut[i+3]=='/' - && unixBackupDir(zOut, &j) - ){ - i += 2; - continue; - } - } - if( ALWAYS(j>=0) ) zOut[j] = zOut[i]; - j++; - } - if( NEVER(j==0) ) zOut[j++] = '/'; - zOut[j] = 0; - return SQLITE_OK; + j = i+1; + }while( zPath[i++] ); } /* @@ -6520,86 +6533,27 @@ static int unixFullPathname( int nOut, /* Size of output buffer in bytes */ char *zOut /* Output buffer */ ){ -#if !defined(HAVE_READLINK) || !defined(HAVE_LSTAT) - return mkFullPathname(zPath, zOut, nOut); -#else - int rc = SQLITE_OK; - int nByte; - int nLink = 0; /* Number of symbolic links followed so far */ - const char *zIn = zPath; /* Input path for each iteration of loop */ - char *zDel = 0; - - assert( pVfs->mxPathname==MAX_PATHNAME ); + DbPath path; UNUSED_PARAMETER(pVfs); - - /* It's odd to simulate an io-error here, but really this is just - ** using the io-error infrastructure to test that SQLite handles this - ** function failing. This function could fail if, for example, the - ** current working directory has been unlinked. - */ - SimulateIOError( return SQLITE_ERROR ); - - do { - - /* Call stat() on path zIn. Set bLink to true if the path is a symbolic - ** link, or false otherwise. */ - int bLink = 0; - struct stat buf; - if( osLstat(zIn, &buf)!=0 ){ - if( errno!=ENOENT ){ - rc = unixLogError(SQLITE_CANTOPEN_BKPT, "lstat", zIn); - } - }else{ - bLink = S_ISLNK(buf.st_mode); + path.rc = 0; + path.nUsed = 0; + path.nSymlink = 0; + path.nOut = nOut; + path.zOut = zOut; + if( zPath[0]!='/' ){ + char zPwd[SQLITE_MAX_PATHLEN+2]; + if( osGetcwd(zPwd, sizeof(zPwd)-2)==0 ){ + return unixLogError(SQLITE_CANTOPEN_BKPT, "getcwd", zPath); } - - if( bLink ){ - nLink++; - if( zDel==0 ){ - zDel = sqlite3_malloc(nOut); - if( zDel==0 ) rc = SQLITE_NOMEM_BKPT; - }else if( nLink>=SQLITE_MAX_SYMLINKS ){ - rc = SQLITE_CANTOPEN_BKPT; - } - - if( rc==SQLITE_OK ){ - nByte = osReadlink(zIn, zDel, nOut-1); - if( nByte<0 ){ - rc = unixLogError(SQLITE_CANTOPEN_BKPT, "readlink", zIn); - }else{ - if( zDel[0]!='/' ){ - int n; - for(n = sqlite3Strlen30(zIn); n>0 && zIn[n-1]!='/'; n--); - if( nByte+n+1>nOut ){ - rc = SQLITE_CANTOPEN_BKPT; - }else{ - memmove(&zDel[n], zDel, nByte+1); - memcpy(zDel, zIn, n); - nByte += n; - } - } - zDel[nByte] = '\0'; - } - } - - zIn = zDel; - } - - assert( rc!=SQLITE_OK || zIn!=zOut || zIn[0]=='/' ); - if( rc==SQLITE_OK && zIn!=zOut ){ - rc = mkFullPathname(zIn, zOut, nOut); - } - if( bLink==0 ) break; - zIn = zOut; - }while( rc==SQLITE_OK ); - - sqlite3_free(zDel); - if( rc==SQLITE_OK && nLink ) rc = SQLITE_OK_SYMLINK; - return rc; -#endif /* HAVE_READLINK && HAVE_LSTAT */ + appendAllPathElements(&path, zPwd); + } + appendAllPathElements(&path, zPath); + zOut[path.nUsed] = 0; + if( path.rc || path.nUsed<2 ) return SQLITE_CANTOPEN_BKPT; + if( path.nSymlink ) return SQLITE_OK_SYMLINK; + return SQLITE_OK; } - #ifndef SQLITE_OMIT_LOAD_EXTENSION /* ** Interfaces for opening a shared library, finding entry points diff --git a/src/parse.y b/src/parse.y index 37560a2e0b..d627f22ba2 100644 --- a/src/parse.y +++ b/src/parse.y @@ -924,7 +924,18 @@ where_opt_ret(A) ::= WHERE expr(X) RETURNING selcollist(Y). cmd ::= with UPDATE orconf(R) xfullname(X) indexed_opt(I) SET setlist(Y) from(F) where_opt_ret(W) orderby_opt(O) limit_opt(L). { sqlite3SrcListIndexedBy(pParse, X, &I); - X = sqlite3SrcListAppendList(pParse, X, F); + if( F ){ + SrcList *pFromClause = F; + if( pFromClause->nSrc>1 ){ + Select *pSubquery; + Token as; + pSubquery = sqlite3SelectNew(pParse,0,pFromClause,0,0,0,0,SF_NestedFrom,0); + as.n = 0; + as.z = 0; + pFromClause = sqlite3SrcListAppendFromTerm(pParse,0,0,0,&as,pSubquery,0); + } + X = sqlite3SrcListAppendList(pParse, X, pFromClause); + } sqlite3ExprListCheckLength(pParse,Y,"set list"); #ifndef SQLITE_ENABLE_UPDATE_DELETE_LIMIT if( O || L ){ @@ -940,7 +951,18 @@ cmd ::= with UPDATE orconf(R) xfullname(X) indexed_opt(I) SET setlist(Y) from(F) where_opt_ret(W). { sqlite3SrcListIndexedBy(pParse, X, &I); sqlite3ExprListCheckLength(pParse,Y,"set list"); - X = sqlite3SrcListAppendList(pParse, X, F); + if( F ){ + SrcList *pFromClause = F; + if( pFromClause->nSrc>1 ){ + Select *pSubquery; + Token as; + pSubquery = sqlite3SelectNew(pParse,0,pFromClause,0,0,0,0,SF_NestedFrom,0); + as.n = 0; + as.z = 0; + pFromClause = sqlite3SrcListAppendFromTerm(pParse,0,0,0,&as,pSubquery,0); + } + X = sqlite3SrcListAppendList(pParse, X, pFromClause); + } sqlite3Update(pParse,X,Y,W,R,0,0,0); } %endif @@ -1218,6 +1240,14 @@ expr(A) ::= expr(A) IS NOT expr(Y). { A = sqlite3PExpr(pParse,TK_ISNOT,A,Y); binaryToUnaryIfNull(pParse, Y, A, TK_NOTNULL); } +expr(A) ::= expr(A) IS NOT DISTINCT FROM expr(Y). { + A = sqlite3PExpr(pParse,TK_IS,A,Y); + binaryToUnaryIfNull(pParse, Y, A, TK_ISNULL); +} +expr(A) ::= expr(A) IS DISTINCT FROM expr(Y). { + A = sqlite3PExpr(pParse,TK_ISNOT,A,Y); + binaryToUnaryIfNull(pParse, Y, A, TK_NOTNULL); +} expr(A) ::= NOT(B) expr(X). {A = sqlite3PExpr(pParse, @B, X, 0);/*A-overwrites-B*/} @@ -1263,7 +1293,8 @@ expr(A) ::= expr(A) between_op(N) expr(X) AND expr(Y). [BETWEEN] { ** regardless of the value of expr1. */ sqlite3ExprUnmapAndDelete(pParse, A); - A = sqlite3Expr(pParse->db, TK_INTEGER, N ? "1" : "0"); + A = sqlite3Expr(pParse->db, TK_STRING, N ? "true" : "false"); + if( A ) sqlite3ExprIdToTrueFalse(A); }else{ Expr *pRHS = Y->a[0].pExpr; if( Y->nExpr==1 && sqlite3ExprIsConstant(pRHS) && A->op!=TK_VECTOR ){ diff --git a/src/printf.c b/src/printf.c index f55b6ef660..f0bfa53279 100644 --- a/src/printf.c +++ b/src/printf.c @@ -954,7 +954,9 @@ void sqlite3RecordErrorByteOffset(sqlite3 *db, const char *z){ ** as the error offset. */ void sqlite3RecordErrorOffsetOfExpr(sqlite3 *db, const Expr *pExpr){ - while( pExpr && (ExprHasProperty(pExpr,EP_FromJoin) || pExpr->w.iOfst<=0) ){ + while( pExpr + && (ExprHasProperty(pExpr,EP_OuterON|EP_InnerON) || pExpr->w.iOfst<=0) + ){ pExpr = pExpr->pLeft; } if( pExpr==0 ) return; diff --git a/src/resolve.c b/src/resolve.c index d7e32911e9..175f81a10e 100644 --- a/src/resolve.c +++ b/src/resolve.c @@ -928,7 +928,7 @@ static int resolveExprStep(Walker *pWalker, Expr *pExpr){ } sqlite3WalkExpr(pWalker, pExpr->pLeft); if( 0==sqlite3ExprCanBeNull(pExpr->pLeft) && !IN_RENAME_OBJECT ){ - testcase( ExprHasProperty(pExpr, EP_FromJoin) ); + testcase( ExprHasProperty(pExpr, EP_OuterON) ); assert( !ExprHasProperty(pExpr, EP_IntValue) ); if( pExpr->op==TK_NOTNULL ){ pExpr->u.zToken = "true"; diff --git a/src/select.c b/src/select.c index b53c0c9151..cc5e4df8db 100644 --- a/src/select.c +++ b/src/select.c @@ -378,11 +378,11 @@ static int tableAndColumnIndex( } /* -** Set the EP_FromJoin property on all terms of the given expression. +** Set the EP_OuterON property on all terms of the given expression. ** And set the Expr.w.iJoin to iTable for every term in the ** expression. ** -** The EP_FromJoin property is used on terms of an expression to tell +** The EP_OuterON property is used on terms of an expression to tell ** the OUTER JOIN processing logic that this term is part of the ** join restriction specified in the ON or USING clause and not a part ** of the more general WHERE clause. These terms are moved over to the @@ -404,7 +404,7 @@ static int tableAndColumnIndex( ** the output, which is incorrect. */ void sqlite3SetJoinExpr(Expr *p, int iTable, u32 joinFlag){ - assert( joinFlag==EP_FromJoin || joinFlag==EP_InnerJoin ); + assert( joinFlag==EP_OuterON || joinFlag==EP_InnerON ); while( p ){ ExprSetProperty(p, joinFlag); assert( !ExprHasProperty(p, EP_TokenOnly|EP_Reduced) ); @@ -425,17 +425,17 @@ void sqlite3SetJoinExpr(Expr *p, int iTable, u32 joinFlag){ } /* Undo the work of sqlite3SetJoinExpr(). In the expression p, convert every -** term that is marked with EP_FromJoin and w.iJoin==iTable into -** an ordinary term that omits the EP_FromJoin mark. +** term that is marked with EP_OuterON and w.iJoin==iTable into +** an ordinary term that omits the EP_OuterON mark. ** ** This happens when a LEFT JOIN is simplified into an ordinary JOIN. */ static void unsetJoinExpr(Expr *p, int iTable){ while( p ){ - if( ExprHasProperty(p, EP_FromJoin) + if( ExprHasProperty(p, EP_OuterON) && (iTable<0 || p->w.iJoin==iTable) ){ - ExprClearProperty(p, EP_FromJoin); - ExprSetProperty(p, EP_InnerJoin); + ExprClearProperty(p, EP_OuterON); + ExprSetProperty(p, EP_InnerON); } if( p->op==TK_COLUMN && p->iTable==iTable ){ ExprClearProperty(p, EP_CanBeNull); @@ -463,8 +463,8 @@ static void unsetJoinExpr(Expr *p, int iTable){ ** ** * ON and USING clauses result in extra terms being added to the ** WHERE clause to enforce the specified constraints. The extra -** WHERE clause terms will be tagged with EP_FromJoin or -** EP_InnerJoin so that we know that they originated in ON/USING. +** WHERE clause terms will be tagged with EP_OuterON or +** EP_InnerON so that we know that they originated in ON/USING. ** ** The terms of a FROM clause are contained in the Select.pSrc structure. ** The left most table is the first entry in Select.pSrc. The right-most @@ -489,7 +489,7 @@ static int sqlite3ProcessJoin(Parse *pParse, Select *p){ u32 joinType; if( NEVER(pLeft->pTab==0 || pRightTab==0) ) continue; - joinType = (pRight->fg.jointype & JT_OUTER)!=0 ? EP_FromJoin : EP_InnerJoin; + joinType = (pRight->fg.jointype & JT_OUTER)!=0 ? EP_OuterON : EP_InnerON; /* If this is a NATURAL join, synthesize an approprate USING clause ** to specify which columns should be joined. @@ -2193,10 +2193,11 @@ int sqlite3ColumnsFromExprList( } if( pColExpr->op==TK_COLUMN && ALWAYS( ExprUseYTab(pColExpr) ) - && (pTab = pColExpr->y.pTab)!=0 + && ALWAYS( pColExpr->y.pTab!=0 ) ){ /* For columns use the column name name */ int iCol = pColExpr->iColumn; + pTab = pColExpr->y.pTab; if( iCol<0 ) iCol = pTab->iPKey; zName = iCol>=0 ? pTab->aCol[iCol].zCnName : "rowid"; }else if( pColExpr->op==TK_ID ){ @@ -3762,7 +3763,7 @@ static Expr *substExpr( Expr *pExpr /* Expr in which substitution occurs */ ){ if( pExpr==0 ) return 0; - if( ExprHasProperty(pExpr, EP_FromJoin) + if( ExprHasProperty(pExpr, EP_OuterON) && pExpr->w.iJoin==pSubst->iTable ){ pExpr->w.iJoin = pSubst->iNewTable; @@ -3803,9 +3804,9 @@ static Expr *substExpr( if( pSubst->isOuterJoin ){ ExprSetProperty(pNew, EP_CanBeNull); } - if( ExprHasProperty(pExpr,EP_FromJoin|EP_InnerJoin) ){ + if( ExprHasProperty(pExpr,EP_OuterON|EP_InnerON) ){ sqlite3SetJoinExpr(pNew, pExpr->w.iJoin, - pExpr->flags & (EP_FromJoin|EP_InnerJoin)); + pExpr->flags & (EP_OuterON|EP_InnerON)); } sqlite3ExprDelete(db, pExpr); pExpr = pNew; @@ -3969,7 +3970,7 @@ static int renumberCursorsCb(Walker *pWalker, Expr *pExpr){ if( op==TK_COLUMN || op==TK_IF_NULL_ROW ){ renumberCursorDoMapping(pWalker, &pExpr->iTable); } - if( ExprHasProperty(pExpr, EP_FromJoin) ){ + if( ExprHasProperty(pExpr, EP_OuterON) ){ renumberCursorDoMapping(pWalker, &pExpr->w.iJoin); } return WRC_Continue; @@ -4547,7 +4548,7 @@ static int flattenSubquery( pWhere = pSub->pWhere; pSub->pWhere = 0; if( isOuterJoin>0 ){ - sqlite3SetJoinExpr(pWhere, iNewParent, EP_FromJoin); + sqlite3SetJoinExpr(pWhere, iNewParent, EP_OuterON); } if( pWhere ){ if( pParent->pWhere ){ @@ -4680,7 +4681,7 @@ static void constInsert( static void findConstInWhere(WhereConst *pConst, Expr *pExpr){ Expr *pRight, *pLeft; if( NEVER(pExpr==0) ) return; - if( ExprHasProperty(pExpr, EP_FromJoin) ) return; + if( ExprHasProperty(pExpr, EP_OuterON) ) return; if( pExpr->op==TK_AND ){ findConstInWhere(pConst, pExpr->pRight); findConstInWhere(pConst, pExpr->pLeft); @@ -4716,9 +4717,9 @@ static int propagateConstantExprRewriteOne( int i; if( pConst->pOomFault[0] ) return WRC_Prune; if( pExpr->op!=TK_COLUMN ) return WRC_Continue; - if( ExprHasProperty(pExpr, EP_FixedCol|EP_FromJoin) ){ + if( ExprHasProperty(pExpr, EP_FixedCol|EP_OuterON) ){ testcase( ExprHasProperty(pExpr, EP_FixedCol) ); - testcase( ExprHasProperty(pExpr, EP_FromJoin) ); + testcase( ExprHasProperty(pExpr, EP_OuterON) ); return WRC_Continue; } for(i=0; inConst; i++){ @@ -5003,12 +5004,12 @@ static int pushDownWhereTerms( #if 0 /* Legacy code. Checks now done by sqlite3ExprIsTableConstraint() */ if( isLeftJoin - && (ExprHasProperty(pWhere,EP_FromJoin)==0 + && (ExprHasProperty(pWhere,EP_OuterON)==0 || pWhere->w.iJoin!=iCursor) ){ return 0; /* restriction (4) */ } - if( ExprHasProperty(pWhere,EP_FromJoin) + if( ExprHasProperty(pWhere,EP_OuterON) && pWhere->w.iJoin!=iCursor ){ return 0; /* restriction (5) */ @@ -6518,6 +6519,29 @@ static int countOfViewOptimization(Parse *pParse, Select *p){ } #endif /* SQLITE_COUNTOFVIEW_OPTIMIZATION */ +/* +** If any term of pSrc, or any SF_NestedFrom sub-query, is not the same +** as pSrcItem but has the same alias as p0, then return true. +** Otherwise return false. +*/ +static int sameSrcAlias(SrcItem *p0, SrcList *pSrc){ + int i; + for(i=0; inSrc; i++){ + SrcItem *p1 = &pSrc->a[i]; + if( p1==p0 ) continue; + if( p0->pTab==p1->pTab && 0==sqlite3_stricmp(p0->zAlias, p1->zAlias) ){ + return 1; + } + if( p1->pSelect + && (p1->pSelect->selFlags & SF_NestedFrom)!=0 + && sameSrcAlias(p0, p1->pSelect->pSrc) + ){ + return 1; + } + } + return 0; +} + /* ** Generate code for the SELECT statement given in the p argument. ** @@ -6622,15 +6646,12 @@ int sqlite3Select( ** disallow it altogether. */ if( p->selFlags & SF_UFSrcCheck ){ SrcItem *p0 = &p->pSrc->a[0]; - for(i=1; ipSrc->nSrc; i++){ - SrcItem *p1 = &p->pSrc->a[i]; - if( p0->pTab==p1->pTab && 0==sqlite3_stricmp(p0->zAlias, p1->zAlias) ){ - sqlite3ErrorMsg(pParse, - "target object/alias may not appear in FROM clause: %s", - p0->zAlias ? p0->zAlias : p0->pTab->zName - ); - goto select_end; - } + if( sameSrcAlias(p0, p->pSrc) ){ + sqlite3ErrorMsg(pParse, + "target object/alias may not appear in FROM clause: %s", + p0->zAlias ? p0->zAlias : p0->pTab->zName + ); + goto select_end; } /* Clear the SF_UFSrcCheck flag. The check has already been performed, @@ -6905,7 +6926,7 @@ int sqlite3Select( /* Generate code to implement the subquery ** - ** The subquery is implemented as a co-routine all of the following are + ** The subquery is implemented as a co-routine all if the following are ** true: ** ** (1) the subquery is guaranteed to be the outer loop (so that @@ -6963,11 +6984,11 @@ int sqlite3Select( ** the same view can reuse the materialization. */ int topAddr; int onceAddr = 0; - int retAddr; pItem->regReturn = ++pParse->nMem; - topAddr = sqlite3VdbeAddOp2(v, OP_Integer, 0, pItem->regReturn); + topAddr = sqlite3VdbeAddOp0(v, OP_Goto); pItem->addrFillSub = topAddr+1; + pItem->fg.isMaterialized = 1; if( pItem->fg.isCorrelated==0 ){ /* If the subquery is not correlated and if we are not inside of ** a trigger, then we only need to compute the value of the subquery @@ -6982,9 +7003,9 @@ int sqlite3Select( sqlite3Select(pParse, pSub, &dest); pItem->pTab->nRowLogEst = pSub->nSelectRow; if( onceAddr ) sqlite3VdbeJumpHere(v, onceAddr); - retAddr = sqlite3VdbeAddOp1(v, OP_Return, pItem->regReturn); + sqlite3VdbeAddOp2(v, OP_Return, pItem->regReturn, topAddr+1); VdbeComment((v, "end %!S", pItem)); - sqlite3VdbeChangeP1(v, topAddr, retAddr); + sqlite3VdbeJumpHere(v, topAddr); sqlite3ClearTempRegCache(pParse); if( pItem->fg.isCte && pItem->fg.isCorrelated==0 ){ CteUse *pCteUse = pItem->u2.pCteUse; @@ -7707,7 +7728,7 @@ int sqlite3Select( eDist = sqlite3WhereIsDistinct(pWInfo); updateAccumulator(pParse, regAcc, pAggInfo, eDist); if( eDist!=WHERE_DISTINCT_NOOP ){ - struct AggInfo_func *pF = &pAggInfo->aFunc[0]; + struct AggInfo_func *pF = pAggInfo->aFunc; if( pF ){ fixDistinctOpenEph(pParse, eDist, pF->iDistinct, pF->iDistAddr); } diff --git a/src/shell.c.in b/src/shell.c.in index 95a32f5247..b03c96183d 100644 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -229,6 +229,16 @@ static void setTextMode(FILE *file, int isOutput){ # define setTextMode(X,Y) #endif +/* +** When compiling with emcc (a.k.a. emscripten), we're building a +** WebAssembly (WASM) bundle and need to disable and rewire a few +** things. +*/ +#ifdef __EMSCRIPTEN__ +#define SQLITE_SHELL_WASM_MODE +#else +#undef SQLITE_SHELL_WASM_MODE +#endif /* True if the timer is enabled */ static int enableTimer = 0; @@ -691,6 +701,7 @@ static char *local_getline(char *zLine, FILE *in){ ** be freed by the caller or else passed back into this routine via the ** zPrior argument for reuse. */ +#ifndef SQLITE_SHELL_WASM_MODE static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ char *zPrompt; char *zResult; @@ -710,7 +721,7 @@ static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ } return zResult; } - +#endif /* !SQLITE_SHELL_WASM_MODE */ /* ** Return the value of a hexadecimal digit. Return -1 if the input @@ -798,7 +809,7 @@ static void freeText(ShellText *p){ ** If the third argument, quote, is not '\0', then it is used as a ** quote character for zAppend. */ -static void appendText(ShellText *p, char const *zAppend, char quote){ +static void appendText(ShellText *p, const char *zAppend, char quote){ int len; int i; int nAppend = strlen30(zAppend); @@ -1009,16 +1020,18 @@ INCLUDE test_windirent.h INCLUDE test_windirent.c #define dirent DIRENT #endif -INCLUDE ../ext/misc/shathree.c -INCLUDE ../ext/misc/fileio.c -INCLUDE ../ext/misc/completion.c -INCLUDE ../ext/misc/appendvfs.c INCLUDE ../ext/misc/memtrace.c +INCLUDE ../ext/misc/shathree.c INCLUDE ../ext/misc/uint.c INCLUDE ../ext/misc/decimal.c INCLUDE ../ext/misc/ieee754.c INCLUDE ../ext/misc/series.c INCLUDE ../ext/misc/regexp.c +#ifndef SQLITE_SHELL_WASM_MODE +INCLUDE ../ext/misc/fileio.c +INCLUDE ../ext/misc/completion.c +INCLUDE ../ext/misc/appendvfs.c +#endif #ifdef SQLITE_HAVE_ZLIB INCLUDE ../ext/misc/zipfile.c INCLUDE ../ext/misc/sqlar.c @@ -1149,8 +1162,18 @@ struct ShellState { char *zNonce; /* Nonce for temporary safe-mode excapes */ EQPGraph sGraph; /* Information for the graphical EXPLAIN QUERY PLAN */ ExpertInfo expert; /* Valid if previous command was ".expert OPT..." */ +#ifdef SQLITE_SHELL_WASM_MODE + struct { + const char * zInput; /* Input string from wasm/JS proxy */ + const char * zPos; /* Cursor pos into zInput */ + } wasm; +#endif }; +#ifdef SQLITE_SHELL_WASM_MODE +static ShellState shellState; +#endif + /* Allowed values for ShellState.autoEQP */ @@ -1191,7 +1214,7 @@ struct ShellState { #define SHFLG_PreserveRowid 0x00000008 /* .dump preserves rowid values */ #define SHFLG_Newlines 0x00000010 /* .dump --newline flag */ #define SHFLG_CountChanges 0x00000020 /* .changes setting */ -#define SHFLG_Echo 0x00000040 /* .echo or --echo setting */ +#define SHFLG_Echo 0x00000040 /* .echo on/off, or --echo setting */ #define SHFLG_HeaderSet 0x00000080 /* showHeader has been specified */ #define SHFLG_DumpDataOnly 0x00000100 /* .dump show data only */ #define SHFLG_DumpNoSys 0x00000200 /* .dump omits system tables */ @@ -3820,11 +3843,6 @@ static int shell_exec( pArg->cnt = 0; } - /* echo the sql statement if echo on */ - if( pArg && ShellHasFlag(pArg, SHFLG_Echo) ){ - utf8_printf(pArg->out, "%s\n", zStmtSql ? zStmtSql : zSql); - } - /* Show the EXPLAIN QUERY PLAN if .eqp is on */ if( pArg && pArg->autoEQP && sqlite3_stmt_isexplain(pStmt)==0 ){ sqlite3_stmt *pExplain; @@ -4223,13 +4241,14 @@ static int run_schema_dump_query( ** Text of help messages. ** ** The help text for each individual command begins with a line that starts -** with ".". Subsequent lines are supplimental information. +** with ".". Subsequent lines are supplemental information. ** ** There must be two or more spaces between the end of the command and the ** start of the description of what that command does. */ static const char *(azHelp[]) = { -#if defined(SQLITE_HAVE_ZLIB) && !defined(SQLITE_OMIT_VIRTUALTABLE) +#if defined(SQLITE_HAVE_ZLIB) && !defined(SQLITE_OMIT_VIRTUALTABLE) \ + && !defined(SQLITE_SHELL_WASM_MODE) ".archive ... Manage SQL archives", " Each command must have exactly one of the following options:", " -c, --create Create a new archive", @@ -4255,16 +4274,22 @@ static const char *(azHelp[]) = { #ifndef SQLITE_OMIT_AUTHORIZATION ".auth ON|OFF Show authorizer callbacks", #endif +#ifndef SQLITE_SHELL_WASM_MODE ".backup ?DB? FILE Backup DB (default \"main\") to FILE", " Options:", " --append Use the appendvfs", " --async Write to FILE without journal and fsync()", +#endif ".bail on|off Stop after hitting an error. Default OFF", ".binary on|off Turn binary output on or off. Default OFF", +#ifndef SQLITE_SHELL_WASM_MODE ".cd DIRECTORY Change the working directory to DIRECTORY", +#endif ".changes on|off Show number of rows changed by SQL", +#ifndef SQLITE_SHELL_WASM_MODE ".check GLOB Fail if output since .testcase does not match", ".clone NEWDB Clone data into NEWDB from the existing database", +#endif ".connection [close] [#] Open or close an auxiliary database connection", ".databases List names and files of attached databases", ".dbconfig ?op? ?val? List or change sqlite3_db_config() options", @@ -4285,9 +4310,13 @@ static const char *(azHelp[]) = { " trace Like \"full\" but enable \"PRAGMA vdbe_trace\"", #endif " trigger Like \"full\" but also show trigger bytecode", +#ifndef SQLITE_SHELL_WASM_MODE ".excel Display the output of next command in spreadsheet", " --bom Put a UTF8 byte-order mark on intermediate file", +#endif +#ifndef SQLITE_SHELL_WASM_MODE ".exit ?CODE? Exit this program with return-code CODE", +#endif ".expert EXPERIMENTAL. Suggest indexes for queries", ".explain ?on|off|auto? Change the EXPLAIN formatting mode. Default: auto", ".filectrl CMD ... Run various sqlite3_file_control() operations", @@ -4296,6 +4325,7 @@ static const char *(azHelp[]) = { ".fullschema ?--indent? Show schema and the content of sqlite_stat tables", ".headers on|off Turn display of headers on or off", ".help ?-all? ?PATTERN? Show help text for PATTERN", +#ifndef SQLITE_SHELL_WASM_MODE ".import FILE TABLE Import data from FILE into TABLE", " Options:", " --ascii Use \\037 and \\036 as column and row separators", @@ -4310,6 +4340,7 @@ static const char *(azHelp[]) = { " from the \".mode\" output mode", " * If FILE begins with \"|\" then it is a command that generates the", " input text.", +#endif #ifndef SQLITE_OMIT_TEST_CONTROL ".imposter INDEX TABLE Create imposter table TABLE on index INDEX", #endif @@ -4323,10 +4354,12 @@ static const char *(azHelp[]) = { ".lint OPTIONS Report potential schema issues.", " Options:", " fkey-indexes Find missing foreign key indexes", -#ifndef SQLITE_OMIT_LOAD_EXTENSION +#if !defined(SQLITE_OMIT_LOAD_EXTENSION) && !defined(SQLITE_SHELL_WASM_MODE) ".load FILE ?ENTRY? Load an extension library", #endif +#ifndef SQLITE_SHELL_WASM_MODE ".log FILE|off Turn logging on or off. FILE can be stderr/stdout", +#endif ".mode MODE ?OPTIONS? Set output mode", " MODE is one of:", " ascii Columns/rows delimited by 0x1F and 0x1E", @@ -4351,16 +4384,23 @@ static const char *(azHelp[]) = { " --quote Quote output text as SQL literals", " --noquote Do not quote output text", " TABLE The name of SQL table used for \"insert\" mode", +#ifndef SQLITE_SHELL_WASM_MODE ".nonce STRING Suspend safe mode for one command if nonce matches", +#endif ".nullvalue STRING Use STRING in place of NULL values", +#ifndef SQLITE_SHELL_WASM_MODE ".once ?OPTIONS? ?FILE? Output for the next SQL command only to FILE", " If FILE begins with '|' then open as a pipe", " --bom Put a UTF8 byte-order mark at the beginning", " -e Send output to the system text editor", " -x Send output as CSV to a spreadsheet (same as \".excel\")", + /* Note that .open is (partially) available in WASM builds but is + ** currently only intended to be used by the fiddle tool, not + ** end users, so is "undocumented." */ ".open ?OPTIONS? ?FILE? Close existing database and reopen FILE", " Options:", " --append Use appendvfs to append database to the end of FILE", +#endif #ifndef SQLITE_OMIT_DESERIALIZE " --deserialize Load into memory using sqlite3_deserialize()", " --hexdb Load the output of \"dbtotxt\" as an in-memory db", @@ -4370,12 +4410,14 @@ static const char *(azHelp[]) = { " --nofollow Do not follow symbolic links", " --readonly Open FILE readonly", " --zip FILE is a ZIP archive", +#ifndef SQLITE_SHELL_WASM_MODE ".output ?FILE? Send output to FILE or stdout if FILE is omitted", " If FILE begins with '|' then open it as a pipe.", " 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", +#endif ".parameter CMD ... Manage SQL parameter bindings", " clear Erase all bindings", " init Initialize the TEMP table that holds bindings", @@ -4392,9 +4434,11 @@ static const char *(azHelp[]) = { " --reset Reset the count for each input and interrupt", #endif ".prompt MAIN CONTINUE Replace the standard prompts", +#ifndef SQLITE_SHELL_WASM_MODE ".quit Exit this program", ".read FILE Read input from FILE or command output", " If FILE begins with \"|\", it is a command that generates the input.", +#endif #if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) ".recover Recover as much data as possible from corrupt db.", " --freelist-corrupt Assume the freelist is corrupt", @@ -4403,8 +4447,10 @@ static const char *(azHelp[]) = { " --no-rowids Do not attempt to recover rowid values", " that are not also INTEGER PRIMARY KEYs", #endif +#ifndef SQLITE_SHELL_WASM_MODE ".restore ?DB? FILE Restore content of DB (default \"main\") from FILE", ".save ?OPTIONS? FILE Write database to FILE (an alias for .backup ...)", +#endif ".scanstats on|off Turn sqlite3_stmt_scanstatus() metrics on or off", ".schema ?PATTERN? Show the CREATE statements matching PATTERN", " Options:", @@ -4438,7 +4484,7 @@ static const char *(azHelp[]) = { " --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", -#ifndef SQLITE_NOHAVE_SYSTEM +#if !defined(SQLITE_NOHAVE_SYSTEM) && !defined(SQLITE_SHELL_WASM_MODE) ".shell CMD ARGS... Run CMD ARGS... in a system shell", #endif ".show Show the current values for various settings", @@ -4447,11 +4493,13 @@ static const char *(azHelp[]) = { " on Turn on automatic stat display", " stmt Show statement stats", " vmstep Show the virtual machine step count only", -#ifndef SQLITE_NOHAVE_SYSTEM +#if !defined(SQLITE_NOHAVE_SYSTEM) && !defined(SQLITE_SHELL_WASM_MODE) ".system CMD ARGS... Run CMD ARGS... in a system shell", #endif ".tables ?TABLE? List names of tables matching LIKE pattern TABLE", +#ifndef SQLITE_SHELL_WASM_MODE ".testcase NAME Begin redirecting output to 'testcase-out.txt'", +#endif ".testctrl CMD ... Run various sqlite3_test_control() operations", " Run \".testctrl\" with no arguments for details", ".timeout MS Try opening locked tables for MS milliseconds", @@ -4996,14 +5044,16 @@ static void open_db(ShellState *p, int openFlags){ #ifndef SQLITE_OMIT_LOAD_EXTENSION sqlite3_enable_load_extension(p->db, 1); #endif - sqlite3_fileio_init(p->db, 0, 0); sqlite3_shathree_init(p->db, 0, 0); - sqlite3_completion_init(p->db, 0, 0); sqlite3_uint_init(p->db, 0, 0); sqlite3_decimal_init(p->db, 0, 0); sqlite3_regexp_init(p->db, 0, 0); sqlite3_ieee_init(p->db, 0, 0); sqlite3_series_init(p->db, 0, 0); +#ifndef SQLITE_SHELL_WASM_MODE + sqlite3_fileio_init(p->db, 0, 0); + sqlite3_completion_init(p->db, 0, 0); +#endif #if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) sqlite3_dbdata_init(p->db, 0, 0); #endif @@ -8126,7 +8176,8 @@ static int do_meta_command(char *zLine, ShellState *p){ }else #endif -#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB) +#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB) \ + && !defined(SQLITE_SHELL_WASM_MODE) if( c=='a' && strncmp(azArg[0], "archive", n)==0 ){ open_db(p, 0); failIfSafeMode(p, "cannot run .archive in safe mode"); @@ -8134,6 +8185,7 @@ static int do_meta_command(char *zLine, ShellState *p){ }else #endif +#ifndef SQLITE_SHELL_WASM_MODE if( (c=='b' && n>=3 && strncmp(azArg[0], "backup", n)==0) || (c=='s' && n>=3 && strncmp(azArg[0], "save", n)==0) ){ @@ -8202,6 +8254,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } close_db(pDest); }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='b' && n>=3 && strncmp(azArg[0], "bail", n)==0 ){ if( nArg==2 ){ @@ -8232,6 +8285,7 @@ static int do_meta_command(char *zLine, ShellState *p){ test_breakpoint(); }else +#ifndef SQLITE_SHELL_WASM_MODE if( c=='c' && strcmp(azArg[0],"cd")==0 ){ failIfSafeMode(p, "cannot run .cd in safe mode"); if( nArg==2 ){ @@ -8251,6 +8305,7 @@ static int do_meta_command(char *zLine, ShellState *p){ rc = 1; } }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='c' && n>=3 && strncmp(azArg[0], "changes", n)==0 ){ if( nArg==2 ){ @@ -8261,6 +8316,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else +#ifndef SQLITE_SHELL_WASM_MODE /* 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. @@ -8285,7 +8341,9 @@ static int do_meta_command(char *zLine, ShellState *p){ } sqlite3_free(zRes); }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ +#ifndef SQLITE_SHELL_WASM_MODE if( c=='c' && strncmp(azArg[0], "clone", n)==0 ){ failIfSafeMode(p, "cannot run .clone in safe mode"); if( nArg==2 ){ @@ -8295,6 +8353,7 @@ static int do_meta_command(char *zLine, ShellState *p){ rc = 1; } }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='c' && strncmp(azArg[0], "connection", n)==0 ){ if( nArg==1 ){ @@ -8437,7 +8496,7 @@ static int do_meta_command(char *zLine, ShellState *p){ int i; int savedShowHeader = p->showHeader; int savedShellFlags = p->shellFlgs; - ShellClearFlag(p, + ShellClearFlag(p, SHFLG_PreserveRowid|SHFLG_Newlines|SHFLG_Echo |SHFLG_DumpDataOnly|SHFLG_DumpNoSys); for(i=1; i1 && (rc = (int)integerValue(azArg[1]))!=0 ) exit(rc); rc = 2; }else +#endif /* The ".explain" command is automatic now. It is largely pointless. It ** retained purely for backwards compatibility */ @@ -8841,6 +8902,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else +#ifndef SQLITE_SHELL_WASM_MODE 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") */ @@ -8862,16 +8924,12 @@ static int do_meta_command(char *zLine, ShellState *p){ 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; } + rc = 1; 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 ){ @@ -8905,7 +8962,6 @@ static int do_meta_command(char *zLine, ShellState *p){ }else{ utf8_printf(p->out, "ERROR: unknown option: \"%s\". Usage:\n", z); showHelp(p->out, "import"); - rc = 1; goto meta_command_exit; } } @@ -8913,7 +8969,6 @@ static int do_meta_command(char *zLine, ShellState *p){ 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; @@ -8925,21 +8980,18 @@ static int do_meta_command(char *zLine, ShellState *p){ 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, + 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 ){ @@ -8953,7 +9005,6 @@ static int do_meta_command(char *zLine, ShellState *p){ 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]; @@ -8964,7 +9015,6 @@ static int do_meta_command(char *zLine, ShellState *p){ 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"); @@ -8977,8 +9027,6 @@ static int do_meta_command(char *zLine, ShellState *p){ } 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) ){ @@ -8992,6 +9040,11 @@ static int do_meta_command(char *zLine, ShellState *p){ output_c_string(p->out, zSep); utf8_printf(p->out, "\n"); } + sCtx.z = sqlite3_malloc64(120); + if( sCtx.z==0 ){ + import_cleanup(&sCtx); + shell_out_of_memory(); + } /* Below, resources must be freed before exit. */ while( (nSkip--)>0 ){ while( xRead(&sCtx) && sCtx.cTerm==sCtx.cColSep ){} @@ -9140,6 +9193,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sCtx.nRow, sCtx.nErr, sCtx.nLine-1); } }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ #ifndef SQLITE_UNTESTABLE if( c=='i' && strncmp(azArg[0], "imposter", n)==0 ){ @@ -9329,7 +9383,7 @@ static int do_meta_command(char *zLine, ShellState *p){ lintDotCommand(p, azArg, nArg); }else -#ifndef SQLITE_OMIT_LOAD_EXTENSION +#if !defined(SQLITE_OMIT_LOAD_EXTENSION) && !defined(SQLITE_SHELL_WASM_MODE) if( c=='l' && strncmp(azArg[0], "load", n)==0 ){ const char *zFile, *zProc; char *zErrMsg = 0; @@ -9351,6 +9405,7 @@ static int do_meta_command(char *zLine, ShellState *p){ }else #endif +#ifndef SQLITE_SHELL_WASM_MODE if( c=='l' && strncmp(azArg[0], "log", n)==0 ){ failIfSafeMode(p, "cannot run .log in safe mode"); if( nArg!=2 ){ @@ -9362,6 +9417,7 @@ static int do_meta_command(char *zLine, ShellState *p){ p->pLog = output_file_open(zFile, 0); } }else +#endif if( c=='m' && strncmp(azArg[0], "mode", n)==0 ){ const char *zMode = 0; @@ -9486,6 +9542,7 @@ static int do_meta_command(char *zLine, ShellState *p){ p->cMode = p->mode; }else +#ifndef SQLITE_SHELL_WASM_MODE if( c=='n' && strcmp(azArg[0], "nonce")==0 ){ if( nArg!=2 ){ raw_printf(stderr, "Usage: .nonce NONCE\n"); @@ -9500,6 +9557,7 @@ static int do_meta_command(char *zLine, ShellState *p){ ** at the end of this procedure */ } }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='n' && strncmp(azArg[0], "nullvalue", n)==0 ){ if( nArg==2 ){ @@ -9521,6 +9579,7 @@ static int do_meta_command(char *zLine, ShellState *p){ /* Check for command-line arguments */ for(iName=1; iNameszMax = integerValue(azArg[++iName]); #endif /* SQLITE_OMIT_DESERIALIZE */ - }else if( z[0]=='-' ){ + }else +#endif /* !SQLITE_SHELL_WASM_MODE */ + if( z[0]=='-' ){ utf8_printf(stderr, "unknown option: %s\n", z); rc = 1; goto meta_command_exit; @@ -9568,6 +9629,7 @@ static int do_meta_command(char *zLine, ShellState *p){ /* If a filename is specified, try to open it first */ if( zFN || p->openMode==SHELL_OPEN_HEXDB ){ if( newFlag && zFN && !p->bSafeMode ) shellDeleteFile(zFN); +#ifndef SQLITE_SHELL_WASM_MODE if( p->bSafeMode && p->openMode!=SHELL_OPEN_HEXDB && zFN @@ -9575,6 +9637,9 @@ static int do_meta_command(char *zLine, ShellState *p){ ){ failIfSafeMode(p, "cannot open disk-based database files in safe mode"); } +#else + /* WASM mode has its own sandboxed pseudo-filesystem. */ +#endif if( zFN ){ zNewFilename = sqlite3_mprintf("%s", zFN); shell_check_oom(zNewFilename); @@ -9597,6 +9662,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else +#ifndef SQLITE_SHELL_WASM_MODE if( (c=='o' && (strncmp(azArg[0], "output", n)==0||strncmp(azArg[0], "once", n)==0)) || (c=='e' && n==5 && strcmp(azArg[0],"excel")==0) @@ -9712,6 +9778,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } sqlite3_free(zFile); }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='p' && n>=3 && strncmp(azArg[0], "parameter", n)==0 ){ open_db(p,0); @@ -9881,10 +9948,13 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else +#ifndef SQLITE_SHELL_WASM_MODE if( c=='q' && strncmp(azArg[0], "quit", n)==0 ){ rc = 2; }else +#endif +#ifndef SQLITE_SHELL_WASM_MODE if( c=='r' && n>=3 && strncmp(azArg[0], "read", n)==0 ){ FILE *inSaved = p->in; int savedLineno = p->lineno; @@ -9919,7 +9989,9 @@ static int do_meta_command(char *zLine, ShellState *p){ p->in = inSaved; p->lineno = savedLineno; }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ +#ifndef SQLITE_SHELL_WASM_MODE if( c=='r' && n>=3 && strncmp(azArg[0], "restore", n)==0 ){ const char *zSrcFile; const char *zDb; @@ -9971,6 +10043,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } close_db(pSrc); }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='s' && strncmp(azArg[0], "scanstats", n)==0 ){ if( nArg==2 ){ @@ -10596,7 +10669,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_free(zSql); }else -#ifndef SQLITE_NOHAVE_SYSTEM +#if !defined(SQLITE_NOHAVE_SYSTEM) && !defined(SQLITE_SHELL_WASM_MODE) if( c=='s' && (strncmp(azArg[0], "shell", n)==0 || strncmp(azArg[0],"system",n)==0) ){ @@ -10617,7 +10690,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_free(zCmd); if( x ) raw_printf(stderr, "System command returns %d\n", x); }else -#endif /* !defined(SQLITE_NOHAVE_SYSTEM) */ +#endif /* !defined(SQLITE_NOHAVE_SYSTEM) && !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='s' && strncmp(azArg[0], "show", n)==0 ){ static const char *azBool[] = { "off", "on", "trigger", "full"}; @@ -10629,7 +10702,7 @@ static int do_meta_command(char *zLine, ShellState *p){ goto meta_command_exit; } utf8_printf(p->out, "%12.12s: %s\n","echo", - azBool[ShellHasFlag(p, SHFLG_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"); @@ -10797,6 +10870,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_free(azResult); }else +#ifndef SQLITE_SHELL_WASM_MODE /* Begin redirecting output to the file "testcase-out.txt" */ if( c=='t' && strcmp(azArg[0],"testcase")==0 ){ output_reset(p); @@ -10810,6 +10884,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "?"); } }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ #ifndef SQLITE_UNTESTABLE if( c=='t' && n>=8 && strncmp(azArg[0], "testctrl", n)==0 ){ @@ -11379,7 +11454,7 @@ static QuickScanState quickscan(char *zLine, QuickScanState qss){ cWait = 0; qss = QSS_SETV(qss, 0); goto PlainScan; - default: assert(0); + default: assert(0); } } } @@ -11400,7 +11475,7 @@ static int line_is_command_terminator(char *zLine){ zLine += 2; /* SQL Server */ else return 0; - return quickscan(zLine,QSS_Start)==QSS_Start; + return quickscan(zLine, QSS_Start)==QSS_Start; } /* @@ -11477,6 +11552,42 @@ static int runOneSqlLine(ShellState *p, char *zSql, FILE *in, int startline){ return 0; } +static void echo_group_input(ShellState *p, const char *zDo){ + if( ShellHasFlag(p, SHFLG_Echo) ) utf8_printf(p->out, "%s\n", zDo); +} + +#ifdef SQLITE_SHELL_WASM_MODE +/* +** Alternate one_input_line() impl for wasm mode. This is not in the primary impl +** because we need the global shellState and cannot access it from that function +** without moving lots of code around (creating a larger/messier diff). +*/ +static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ + /* Parse the next line from shellState.wasm.zInput. */ + const char *zBegin = shellState.wasm.zPos; + const char *z = zBegin; + char *zLine = 0; + int nZ = 0; + + UNUSED_PARAMETER(in); + UNUSED_PARAMETER(isContinuation); + if(!z || !*z){ + return 0; + } + while(*z && isspace(*z)) ++z; + zBegin = z; + for(; *z && '\n'!=*z; ++nZ, ++z){} + if(nZ>0 && '\r'==zBegin[nZ-1]){ + --nZ; + } + shellState.wasm.zPos = z; + zLine = realloc(zPrior, nZ+1); + shell_check_oom(zLine); + memcpy(zLine, zBegin, (size_t)nZ); + zLine[nZ] = 0; + return zLine; +} +#endif /* SQLITE_SHELL_WASM_MODE */ /* ** Read input from *in and process it. If *in==0 then input @@ -11526,14 +11637,13 @@ static int process_input(ShellState *p){ } qss = quickscan(zLine, qss); if( QSS_PLAINWHITE(qss) && nSql==0 ){ - if( ShellHasFlag(p, SHFLG_Echo) ) - printf("%s\n", zLine); /* Just swallow single-line whitespace */ + echo_group_input(p, zLine); qss = QSS_Start; continue; } if( zLine && (zLine[0]=='.' || zLine[0]=='#') && nSql==0 ){ - if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine); + echo_group_input(p, zLine); if( zLine[0]=='.' ){ rc = do_meta_command(zLine, p); if( rc==2 ){ /* exit requested */ @@ -11566,6 +11676,7 @@ static int process_input(ShellState *p){ nSql += nLine; } if( nSql && QSS_SEMITERM(qss) && sqlite3_complete(zSql) ){ + echo_group_input(p, zSql); errCnt += runOneSqlLine(p, zSql, p->in, startline); nSql = 0; if( p->outCount ){ @@ -11577,13 +11688,14 @@ static int process_input(ShellState *p){ p->bSafeMode = p->bSafeModePersist; qss = QSS_Start; }else if( nSql && QSS_PLAINWHITE(qss) ){ - if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zSql); + echo_group_input(p, zSql); nSql = 0; qss = QSS_Start; } } if( nSql ){ /* This may be incomplete. Let the SQL parser deal with that. */ + echo_group_input(p, zSql); errCnt += runOneSqlLine(p, zSql, p->in, startline); } free(zSql); @@ -11722,7 +11834,7 @@ static const char zOptions[] = #if !defined(SQLITE_OMIT_DESERIALIZE) " -deserialize open the database using sqlite3_deserialize()\n" #endif - " -echo print commands before execution\n" + " -echo print inputs before execution\n" " -init FILENAME read/process named file\n" " -[no]header turn headers on or off\n" #if defined(SQLITE_ENABLE_MEMSYS3) || defined(SQLITE_ENABLE_MEMSYS5) @@ -11858,14 +11970,25 @@ static char *cmdline_option_value(int argc, char **argv, int i){ # endif #endif +#ifdef SQLITE_SHELL_WASM_MODE +# define main fiddle_main +#endif + #if SQLITE_SHELL_IS_UTF8 int SQLITE_CDECL main(int argc, char **argv){ #else int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ char **argv; +#endif +#ifdef SQLITE_DEBUG + sqlite3_uint64 mem_main_enter = sqlite3_memory_used(); #endif char *zErrMsg = 0; +#ifdef SQLITE_SHELL_WASM_MODE +# define data shellState +#else ShellState data; +#endif const char *zInitFile = 0; int i; int rc = 0; @@ -11881,8 +12004,13 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ setBinaryMode(stdin, 0); setvbuf(stderr, 0, _IONBF, 0); /* Make sure stderr is unbuffered */ +#ifdef SQLITE_SHELL_WASM_MODE + stdin_is_interactive = 0; + stdout_is_console = 1; +#else stdin_is_interactive = isatty(0); stdout_is_console = isatty(1); +#endif #if !defined(_WIN32_WCE) if( getenv("SQLITE_DEBUG_BREAK") ){ @@ -12138,7 +12266,9 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ #endif } data.out = stdout; +#ifndef SQLITE_SHELL_WASM_MODE sqlite3_appendvfs_init(0,0,0); +#endif /* Go ahead and open the database file if it already exists. If the ** file does not exist, delay opening it. This prevents empty database @@ -12404,6 +12534,9 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ rc = process_input(&data); } } +#ifndef SQLITE_SHELL_WASM_MODE + /* In WASM mode we have to leave the db state in place so that + ** client code can "push" SQL into it after this call returns. */ free(azCmd); set_table_name(&data, 0); if( data.db ){ @@ -12430,5 +12563,136 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ /* Clear the global data structure so that valgrind will detect memory ** leaks */ memset(&data, 0, sizeof(data)); +#ifdef SQLITE_DEBUG + if( sqlite3_memory_used()>mem_main_enter ){ + utf8_printf(stderr, "Memory leaked: %u bytes\n", + (unsigned int)(sqlite3_memory_used()-mem_main_enter)); + } +#endif +#endif /* !SQLITE_SHELL_WASM_MODE */ return rc; } + + +#ifdef SQLITE_SHELL_WASM_MODE +/* Only for emcc experimentation purposes. */ +int fiddle_experiment(int a,int b){ + return a + b; +} + +/* Only for emcc experimentation purposes. + + Define this function in JS using: + + emcc ... --js-library somefile.js + + containing: + +mergeInto(LibraryManager.library, { + my_foo: function(){ + console.debug("my_foo()",arguments); + } +}); +*/ +/*extern void my_foo(sqlite3 *);*/ +/* Only for emcc experimentation purposes. */ +sqlite3 * fiddle_the_db(){ + printf("fiddle_the_db(%p)\n", (const void*)globalDb); + /*my_foo(globalDb);*/ + return globalDb; +} +/* Only for emcc experimentation purposes. */ +sqlite3 * fiddle_db_arg(sqlite3 *arg){ + printf("fiddle_db_arg(%p)\n", (const void*)arg); + return arg; +} + +/* +** Intended to be called via a SharedWorker() while a separate +** SharedWorker() (which manages the wasm module) is performing work +** which should be interrupted. Unfortunately, SharedWorker is not +** portable enough to make real use of. +*/ +void fiddle_interrupt(void){ + if(globalDb) sqlite3_interrupt(globalDb); +} + +/* +** Returns the filename of the given db name, assuming "main" if +** zDbName is NULL. Returns NULL if globalDb is not opened. +*/ +const char * fiddle_db_filename(const char * zDbName){ + return globalDb + ? sqlite3_db_filename(globalDb, zDbName ? zDbName : "main") + : NULL; +} + +/* +** Closes, unlinks, and reopens the db using its current filename (or +** the default if the db is currently closed). It is assumed, for +** purposes of the fiddle build, that the file is in a transient +** virtual filesystem within the browser. +*/ +void fiddle_reset_db(void){ + char *zFilename = 0; + if(0==globalDb){ + shellState.pAuxDb->zDbFilename = "/fiddle.sqlite3"; + }else{ + zFilename = + sqlite3_mprintf("%s", sqlite3_db_filename(globalDb, "main")); + shell_check_oom(zFilename); + close_db(globalDb); + shellDeleteFile(zFilename); + shellState.db = 0; + shellState.pAuxDb->zDbFilename = zFilename; + } + open_db(&shellState, 0); + sqlite3_free(zFilename); +} + +/* +** Trivial exportable function for emscripten. Needs to be exported using: +** +** emcc ..flags... -sEXPORTED_FUNCTIONS=_fiddle_exec -sEXPORTED_RUNTIME_METHODS=ccall,cwrap +** +** (Note the underscore before the function name.) It processes zSql +** as if it were input to the sqlite3 shell and redirects all output +** to the wasm binding. +*/ +void fiddle_exec(const char * zSql){ + static int once = 0; + int rc = 0; + if(!once){ + /* Simulate an argv array for main() */ + static char * argv[] = {"fiddle", + "-bail", + "-safe"}; + rc = fiddle_main((int)(sizeof(argv)/sizeof(argv[0])), argv); + once = rc ? -1 : 1; + memset(&shellState.wasm, 0, sizeof(shellState.wasm)); + printf( + "SQLite version %s %.19s\n" /*extra-version-info*/, + sqlite3_libversion(), sqlite3_sourceid() + ); + puts("WASM shell"); + puts("Enter \".help\" for usage hints."); + if(once>0){ + fiddle_reset_db(); + } + if(shellState.db){ + printf("Connected to %s.\n", fiddle_db_filename(NULL)); + }else{ + fprintf(stderr,"ERROR initializing db!\n"); + return; + } + } + if(once<0){ + puts("DB init failed. Not executing SQL."); + }else if(zSql && *zSql){ + shellState.wasm.zInput = zSql; + shellState.wasm.zPos = zSql; + process_input(&shellState); + memset(&shellState.wasm, 0, sizeof(shellState.wasm)); + } +} +#endif /* SQLITE_SHELL_WASM_MODE */ diff --git a/src/sqlite.h.in b/src/sqlite.h.in index 9c4df2292d..ca56b8cb98 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -6276,6 +6276,28 @@ int sqlite3_get_autocommit(sqlite3*); */ sqlite3 *sqlite3_db_handle(sqlite3_stmt*); +/* +** CAPI3REF: Return The Schema Name For A Database Connection +** METHOD: sqlite3 +** +** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name +** for the N-th database on database connection D, or a NULL pointer of N is +** out of range. An N alue of 0 means the main database file. An N of 1 is +** the "temp" schema. Larger values of N correspond to various ATTACH-ed +** databases. +** +** Space to hold the string that is returned by sqlite3_db_name() is managed +** by SQLite itself. The string might be deallocated by any operation that +** changes the schema, including [ATTACH] or [DETACH] or calls to +** [sqlite3_serialize()] or [sqlite3_deserialize()], even operations that +** occur on a different thread. Applications that need to +** remember the string long-term should make their own copy. Applications that +** are accessing the same database connection simultaneously on multiple +** threads should mutex-protect calls to this API and should make their own +** private copy of the result prior to releasing the mutex. +*/ +const char *sqlite3_db_name(sqlite3 *db, int N); + /* ** CAPI3REF: Return The Filename For A Database Connection ** METHOD: sqlite3 diff --git a/src/sqlite3ext.h b/src/sqlite3ext.h index a75dd496e7..2cdd0e429b 100644 --- a/src/sqlite3ext.h +++ b/src/sqlite3ext.h @@ -356,6 +356,7 @@ struct sqlite3_api_routines { sqlite3_int64,sqlite3_int64,unsigned); unsigned char *(*serialize)(sqlite3*,const char *,sqlite3_int64*, unsigned int); + const char *(*db_name)(sqlite3*,int); }; /* @@ -674,10 +675,12 @@ typedef int (*sqlite3_loadext_entry)( #define sqlite3_vtab_in sqlite3_api->vtab_in #define sqlite3_vtab_in_first sqlite3_api->vtab_in_first #define sqlite3_vtab_in_next sqlite3_api->vtab_in_next +/* Version 3.39.0 and later */ #ifndef SQLITE_OMIT_DESERIALIZE #define sqlite3_deserialize sqlite3_api->deserialize #define sqlite3_serialize sqlite3_api->serialize #endif +#define sqlite3_db_name sqlite3_api->db_name #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */ #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) diff --git a/src/sqliteInt.h b/src/sqliteInt.h index 7cbdb46215..3a585feb5e 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -2847,7 +2847,7 @@ struct Expr { ** TK_SELECT_COLUMN: column of the result vector */ i16 iAgg; /* Which entry in pAggInfo->aCol[] or ->aFunc[] */ union { - int iJoin; /* If EP_FromJoin, the right table of the join */ + int iJoin; /* If EP_OuterON or EP_InnerON, the right table */ int iOfst; /* else: start of token from start of statement */ } w; AggInfo *pAggInfo; /* Used by TK_AGG_COLUMN and TK_AGG_FUNCTION */ @@ -2868,29 +2868,29 @@ struct Expr { ** EP_Agg == NC_HasAgg == SF_HasAgg ** EP_Win == NC_HasWin */ -#define EP_FromJoin 0x000001 /* Originates in ON/USING clause of outer join */ -#define EP_Distinct 0x000002 /* Aggregate function with DISTINCT keyword */ -#define EP_HasFunc 0x000004 /* Contains one or more functions of any kind */ -#define EP_FixedCol 0x000008 /* TK_Column with a known fixed value */ +#define EP_OuterON 0x000001 /* Originates in ON/USING clause of outer join */ +#define EP_InnerON 0x000002 /* Originates in ON/USING of an inner join */ +#define EP_Distinct 0x000004 /* Aggregate function with DISTINCT keyword */ +#define EP_HasFunc 0x000008 /* Contains one or more functions of any kind */ #define EP_Agg 0x000010 /* Contains one or more aggregate functions */ -#define EP_VarSelect 0x000020 /* pSelect is correlated, not constant */ -#define EP_DblQuoted 0x000040 /* token.z was originally in "..." */ -#define EP_InfixFunc 0x000080 /* True for an infix function: LIKE, GLOB, etc */ -#define EP_Collate 0x000100 /* Tree contains a TK_COLLATE operator */ -#define EP_Commuted 0x000200 /* Comparison operator has been commuted */ -#define EP_IntValue 0x000400 /* Integer value contained in u.iValue */ -#define EP_xIsSelect 0x000800 /* x.pSelect is valid (otherwise x.pList is) */ -#define EP_Skip 0x001000 /* Operator does not contribute to affinity */ -#define EP_Reduced 0x002000 /* Expr struct EXPR_REDUCEDSIZE bytes only */ -#define EP_TokenOnly 0x004000 /* Expr struct EXPR_TOKENONLYSIZE bytes only */ +#define EP_FixedCol 0x000020 /* TK_Column with a known fixed value */ +#define EP_VarSelect 0x000040 /* pSelect is correlated, not constant */ +#define EP_DblQuoted 0x000080 /* token.z was originally in "..." */ +#define EP_InfixFunc 0x000100 /* True for an infix function: LIKE, GLOB, etc */ +#define EP_Collate 0x000200 /* Tree contains a TK_COLLATE operator */ +#define EP_Commuted 0x000400 /* Comparison operator has been commuted */ +#define EP_IntValue 0x000800 /* Integer value contained in u.iValue */ +#define EP_xIsSelect 0x001000 /* x.pSelect is valid (otherwise x.pList is) */ +#define EP_Skip 0x002000 /* Operator does not contribute to affinity */ +#define EP_Reduced 0x004000 /* Expr struct EXPR_REDUCEDSIZE bytes only */ #define EP_Win 0x008000 /* Contains window functions */ -#define EP_MemToken 0x010000 /* Need to sqlite3DbFree() Expr.zToken */ -#define EP_IfNullRow 0x020000 /* The TK_IF_NULL_ROW opcode */ -#define EP_Unlikely 0x040000 /* unlikely() or likelihood() function */ -#define EP_ConstFunc 0x080000 /* A SQLITE_FUNC_CONSTANT or _SLOCHNG function */ -#define EP_CanBeNull 0x100000 /* Can be null despite NOT NULL constraint */ -#define EP_Subquery 0x200000 /* Tree contains a TK_SELECT operator */ -#define EP_InnerJoin 0x400000 /* Originates in ON/USING of an inner join */ +#define EP_TokenOnly 0x010000 /* Expr struct EXPR_TOKENONLYSIZE bytes only */ +#define EP_MemToken 0x020000 /* Need to sqlite3DbFree() Expr.zToken */ +#define EP_IfNullRow 0x040000 /* The TK_IF_NULL_ROW opcode */ +#define EP_Unlikely 0x080000 /* unlikely() or likelihood() function */ +#define EP_ConstFunc 0x100000 /* A SQLITE_FUNC_CONSTANT or _SLOCHNG function */ +#define EP_CanBeNull 0x200000 /* Can be null despite NOT NULL constraint */ +#define EP_Subquery 0x400000 /* Tree contains a TK_SELECT operator */ #define EP_Leaf 0x800000 /* Expr.pLeft, .pRight, .u.pSelect all NULL */ #define EP_WinFunc 0x1000000 /* TK_FUNCTION with Expr.y.pWin set */ #define EP_Subrtn 0x2000000 /* Uses Expr.y.sub. TK_IN, _SELECT, or _EXISTS */ @@ -2913,8 +2913,8 @@ struct Expr { #define ExprHasAllProperty(E,P) (((E)->flags&(P))==(P)) #define ExprSetProperty(E,P) (E)->flags|=(P) #define ExprClearProperty(E,P) (E)->flags&=~(P) -#define ExprAlwaysTrue(E) (((E)->flags&(EP_FromJoin|EP_IsTrue))==EP_IsTrue) -#define ExprAlwaysFalse(E) (((E)->flags&(EP_FromJoin|EP_IsFalse))==EP_IsFalse) +#define ExprAlwaysTrue(E) (((E)->flags&(EP_OuterON|EP_IsTrue))==EP_IsTrue) +#define ExprAlwaysFalse(E) (((E)->flags&(EP_OuterON|EP_IsFalse))==EP_IsFalse) /* Macros used to ensure that the correct members of unions are accessed ** in Expr. @@ -3093,6 +3093,7 @@ struct SrcItem { unsigned isIndexedBy :1; /* True if there is an INDEXED BY clause */ unsigned isTabFunc :1; /* True if table-valued-function syntax */ unsigned isCorrelated :1; /* True if sub-query is correlated */ + unsigned isMaterialized:1; /* This is a materialized view */ unsigned viaCoroutine :1; /* Implemented as a co-routine */ unsigned isRecursive :1; /* True for recursive reference in WITH */ unsigned fromDDL :1; /* Comes from sqlite_schema */ @@ -5322,7 +5323,6 @@ const char *sqlite3JournalModename(int); #define IN_INDEX_NOOP_OK 0x0001 /* OK to return IN_INDEX_NOOP */ #define IN_INDEX_MEMBERSHIP 0x0002 /* IN operator used for membership test */ #define IN_INDEX_LOOP 0x0004 /* IN operator used as a loop */ -#define IN_INDEX_REUSE_CUR 0x0008 /* Reuse prior table cursor */ int sqlite3FindInIndex(Parse *, Expr *, u32, int*, int*, int*); int sqlite3JournalOpen(sqlite3_vfs *, const char *, sqlite3_file *, int, int); diff --git a/src/test4.c b/src/test4.c index 5da9cc488d..2043a33830 100644 --- a/src/test4.c +++ b/src/test4.c @@ -60,6 +60,11 @@ struct Thread { #define N_THREAD 26 static Thread threadset[N_THREAD]; +static void test_barrier(){ + sqlite3_mutex *pMutex = sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_APP1); + sqlite3_mutex_enter(pMutex); + sqlite3_mutex_leave(pMutex); +} /* ** The main loop for a thread. Threads use busy waiting. @@ -76,16 +81,20 @@ static void *test_thread_main(void *pArg){ p->db = 0; } p->pStmt = 0; + test_barrier(); p->completed = 1; while( p->opnum<=p->completed ) sched_yield(); + test_barrier(); while( p->xOp ){ if( p->zErr && p->zErr!=p->zStaticErr ){ sqlite3_free(p->zErr); p->zErr = 0; } (*p->xOp)(p); + test_barrier(); p->completed++; while( p->opnum<=p->completed ) sched_yield(); + test_barrier(); } if( p->pStmt ){ sqlite3_finalize(p->pStmt); @@ -99,6 +108,7 @@ static void *test_thread_main(void *pArg){ sqlite3_free(p->zErr); p->zErr = 0; } + test_barrier(); p->completed++; #ifndef SQLITE_OMIT_DEPRECATED sqlite3_thread_cleanup(); @@ -166,7 +176,9 @@ static int SQLITE_TCLAPI tcl_thread_create( ** Wait for a thread to reach its idle state. */ static void test_thread_wait(Thread *p){ + test_barrier(); while( p->opnum>p->completed ) sched_yield(); + test_barrier(); } /* @@ -456,6 +468,7 @@ static int SQLITE_TCLAPI tcl_thread_compile( threadset[i].xOp = do_compile; sqlite3_free(threadset[i].zArg); threadset[i].zArg = sqlite3_mprintf("%s", argv[2]); + test_barrier(); threadset[i].opnum++; return TCL_OK; } @@ -507,6 +520,7 @@ static int SQLITE_TCLAPI tcl_thread_step( } test_thread_wait(&threadset[i]); threadset[i].xOp = do_step; + test_barrier(); threadset[i].opnum++; return TCL_OK; } @@ -551,6 +565,7 @@ static int SQLITE_TCLAPI tcl_thread_finalize( threadset[i].xOp = do_finalize; sqlite3_free(threadset[i].zArg); threadset[i].zArg = 0; + test_barrier(); threadset[i].opnum++; return TCL_OK; } diff --git a/src/treeview.c b/src/treeview.c index 3ff300c38b..90408b0a57 100644 --- a/src/treeview.c +++ b/src/treeview.c @@ -489,8 +489,11 @@ void sqlite3TreeViewExpr(TreeView *pView, const Expr *pExpr, u8 moreToFollow){ sqlite3StrAccumInit(&x, 0, zFlgs, sizeof(zFlgs), 0); sqlite3_str_appendf(&x, " fg.af=%x.%c", pExpr->flags, pExpr->affExpr ? pExpr->affExpr : 'n'); - if( ExprHasProperty(pExpr, EP_FromJoin) ){ - sqlite3_str_appendf(&x, " iJoin=%d", pExpr->w.iJoin); + if( ExprHasProperty(pExpr, EP_OuterON) ){ + sqlite3_str_appendf(&x, " outer.iJoin=%d", pExpr->w.iJoin); + } + if( ExprHasProperty(pExpr, EP_InnerON) ){ + sqlite3_str_appendf(&x, " inner.iJoin=%d", pExpr->w.iJoin); } if( ExprHasProperty(pExpr, EP_FromDDL) ){ sqlite3_str_appendf(&x, " DDL"); diff --git a/src/trigger.c b/src/trigger.c index 42c76bcd93..3b7d0d9e88 100644 --- a/src/trigger.c +++ b/src/trigger.c @@ -514,7 +514,7 @@ TriggerStep *sqlite3TriggerInsertStep( TriggerStep *sqlite3TriggerUpdateStep( Parse *pParse, /* Parser */ Token *pTableName, /* Name of the table to be updated */ - SrcList *pFrom, + SrcList *pFrom, /* FROM clause for an UPDATE-FROM, or NULL */ ExprList *pEList, /* The SET clause: list of column and new values */ Expr *pWhere, /* The WHERE clause */ u8 orconf, /* The conflict algorithm. (OE_Abort, OE_Ignore, etc) */ @@ -850,6 +850,14 @@ SrcList *sqlite3TriggerStepSrc( } if( pStep->pFrom ){ SrcList *pDup = sqlite3SrcListDup(db, pStep->pFrom, 0); + if( pDup && pDup->nSrc>1 && !IN_RENAME_OBJECT ){ + Select *pSubquery; + Token as; + pSubquery = sqlite3SelectNew(pParse,0,pDup,0,0,0,0,SF_NestedFrom,0); + as.n = 0; + as.z = 0; + pDup = sqlite3SrcListAppendFromTerm(pParse,0,0,0,&as,pSubquery,0); + } pSrc = sqlite3SrcListAppendList(pParse, pSrc, pDup); } }else{ diff --git a/src/vdbeaux.c b/src/vdbeaux.c index 9e702edcd5..b2e471e0c9 100644 --- a/src/vdbeaux.c +++ b/src/vdbeaux.c @@ -1576,11 +1576,9 @@ char *sqlite3VdbeDisplayComment( }else if( c=='X' ){ if( pOp->zComment && pOp->zComment[0] ){ sqlite3_str_appendall(&x, pOp->zComment); - }else{ - sqlite3_str_appendall(&x, zSynopsis+1); + seenCom = 1; + break; } - seenCom = 1; - break; }else{ int v1 = translateP(c, pOp); int v2; diff --git a/src/where.c b/src/where.c index a9c6db64e5..d563f7b93b 100644 --- a/src/where.c +++ b/src/where.c @@ -330,7 +330,7 @@ static WhereTerm *whereScanNext(WhereScan *pScan){ && (iColumn!=XN_EXPR || sqlite3ExprCompareSkip(pTerm->pExpr->pLeft, pScan->pIdxExpr,iCur)==0) - && (pScan->iEquiv<=1 || !ExprHasProperty(pTerm->pExpr, EP_FromJoin)) + && (pScan->iEquiv<=1 || !ExprHasProperty(pTerm->pExpr, EP_OuterON)) ){ if( (pTerm->eOperator & WO_EQUIV)!=0 && pScan->nEquivaiCur) @@ -757,7 +757,7 @@ static int termCanDriveIndex( if( pTerm->leftCursor!=pSrc->iCursor ) return 0; if( (pTerm->eOperator & (WO_EQ|WO_IS))==0 ) return 0; if( (pSrc->fg.jointype & (JT_LEFT|JT_LTORJ))!=0 - && !ExprHasProperty(pTerm->pExpr, EP_FromJoin) + && !ExprHasProperty(pTerm->pExpr, EP_OuterON) && (pTerm->eOperator & WO_IS) ){ /* Cannot use an IS term from the WHERE clause as an index driver for @@ -1181,7 +1181,7 @@ static sqlite3_index_info *allocateIndexInfo( ** RIGHT JOIN. See tag-20191211-001 for the ** equivalent restriction for ordinary tables. */ if( (pSrc->fg.jointype & (JT_LEFT|JT_LTORJ))!=0 - && !ExprHasProperty(pTerm->pExpr, EP_FromJoin) + && !ExprHasProperty(pTerm->pExpr, EP_OuterON) ){ continue; } @@ -2059,7 +2059,7 @@ void sqlite3WhereTermPrint(WhereTerm *pTerm, int iTerm){ memcpy(zType, "....", 5); if( pTerm->wtFlags & TERM_VIRTUAL ) zType[0] = 'V'; if( pTerm->eOperator & WO_EQUIV ) zType[1] = 'E'; - if( ExprHasProperty(pTerm->pExpr, EP_FromJoin) ) zType[2] = 'L'; + if( ExprHasProperty(pTerm->pExpr, EP_OuterON) ) zType[2] = 'L'; if( pTerm->wtFlags & TERM_CODED ) zType[3] = 'C'; if( pTerm->eOperator & WO_SINGLE ){ assert( (pTerm->eOperator & (WO_OR|WO_AND))==0 ); @@ -2836,7 +2836,7 @@ static int whereLoopAddBtreeIndex( ** RIGHT JOIN. Only constraints in the ** ON clause are allowed. See tag-20191211-002 for the vtab equivalent. */ if( (pSrc->fg.jointype & (JT_LEFT|JT_LTORJ))!=0 - && !ExprHasProperty(pTerm->pExpr, EP_FromJoin|EP_InnerJoin) + && !ExprHasProperty(pTerm->pExpr, EP_OuterON|EP_InnerON) ){ continue; } @@ -3205,8 +3205,8 @@ static int whereUsablePartialIndex( for(i=0, pTerm=pWC->a; inTerm; i++, pTerm++){ Expr *pExpr; pExpr = pTerm->pExpr; - if( (!ExprHasProperty(pExpr, EP_FromJoin) || pExpr->w.iJoin==iTab) - && (isLeft==0 || ExprHasProperty(pExpr, EP_FromJoin)) + if( (!ExprHasProperty(pExpr, EP_OuterON) || pExpr->w.iJoin==iTab) + && (isLeft==0 || ExprHasProperty(pExpr, EP_OuterON)) && sqlite3ExprImpliesExpr(pParse, pExpr, pWhere, iTab) && (pTerm->wtFlags & TERM_VNULL)==0 ){ @@ -3482,7 +3482,14 @@ static int whereLoopAddBtree( } ApplyCostMultiplier(pNew->rRun, pTab->costMult); whereLoopOutputAdjust(pWC, pNew, rSize); - rc = whereLoopInsert(pBuilder, pNew); + if( (pSrc->fg.jointype & JT_RIGHT)!=0 && pProbe->aColExpr ){ + /* Do not do an SCAN of a index-on-expression in a RIGHT JOIN + ** because the cursor used to access the index might not be + ** positioned to the correct row during the right-join no-match + ** loop. */ + }else{ + rc = whereLoopInsert(pBuilder, pNew); + } pNew->nOut = rSize; if( rc ) break; } @@ -3657,6 +3664,7 @@ static int whereLoopAddVirtualOne( *pbIn = 1; assert( (mExclude & WO_IN)==0 ); } + assert( pbRetryLimit || !isLimitTerm(pTerm) ); if( isLimitTerm(pTerm) && *pbIn ){ /* If there is an IN(...) term handled as an == (separate call to ** xFilter for each value on the RHS of the IN) and a LIMIT or @@ -4130,8 +4138,10 @@ static int whereLoopAddAll(WhereLoopBuilder *pBuilder){ SrcItem *pEnd = &pTabList->a[pWInfo->nLevel]; sqlite3 *db = pWInfo->pParse->db; int rc = SQLITE_OK; + int bFirstPastRJ = 0; WhereLoop *pNew; + /* Loop over the tables in the join, from left to right */ pNew = pBuilder->pNew; whereLoopInit(pNew); @@ -4141,10 +4151,13 @@ static int whereLoopAddAll(WhereLoopBuilder *pBuilder){ pNew->iTab = iTab; pBuilder->iPlanLimit += SQLITE_QUERY_PLANNER_LIMIT_INCR; pNew->maskSelf = sqlite3WhereGetMask(&pWInfo->sMaskSet, pItem->iCursor); - if( (pItem->fg.jointype & (JT_OUTER|JT_CROSS))!=0 ){ - /* This condition is true when pItem is the FROM clause term on the - ** right-hand-side of a OUTER or CROSS JOIN. */ + if( bFirstPastRJ || (pItem->fg.jointype & (JT_OUTER|JT_CROSS))!=0 ){ + /* Add prerequisites to prevent reordering of FROM clause terms + ** across CROSS joins and outer joins. The bFirstPastRJ boolean + ** prevents the right operand of a RIGHT JOIN from being swapped with + ** other elements even further to the right. */ mPrereq |= mPrior; + bFirstPastRJ = (pItem->fg.jointype & JT_RIGHT)!=0; } #ifndef SQLITE_OMIT_VIRTUALTABLE if( IsVirtual(pItem->pTab) ){ @@ -5214,7 +5227,7 @@ static SQLITE_NOINLINE Bitmask whereOmitNoopJoin( pEnd = pWInfo->sWC.a + pWInfo->sWC.nTerm; for(pTerm=pWInfo->sWC.a; pTermprereqAll & pLoop->maskSelf)!=0 ){ - if( !ExprHasProperty(pTerm->pExpr, EP_FromJoin) + if( !ExprHasProperty(pTerm->pExpr, EP_OuterON) || pTerm->pExpr->w.iJoin!=pItem->iCursor ){ break; @@ -5840,6 +5853,7 @@ WhereInfo *sqlite3WhereBegin( iIndexCur = pParse->nTab++; } pLevel->iIdxCur = iIndexCur; + assert( pIx!=0 ); assert( pIx->pSchema==pTab->pSchema ); assert( iIndexCur>=0 ); if( op ){ @@ -5915,9 +5929,20 @@ WhereInfo *sqlite3WhereBegin( for(ii=0; iinErr ) goto whereBeginError; pLevel = &pWInfo->a[ii]; wsFlags = pLevel->pWLoop->wsFlags; + pSrc = &pTabList->a[pLevel->iFrom]; + if( pSrc->fg.isMaterialized ){ + if( pSrc->fg.isCorrelated ){ + sqlite3VdbeAddOp2(v, OP_Gosub, pSrc->regReturn, pSrc->addrFillSub); + }else{ + int iOnce = sqlite3VdbeAddOp0(v, OP_Once); VdbeCoverage(v); + sqlite3VdbeAddOp2(v, OP_Gosub, pSrc->regReturn, pSrc->addrFillSub); + sqlite3VdbeJumpHere(v, iOnce); + } + } if( (wsFlags & (WHERE_AUTO_INDEX|WHERE_BLOOMFILTER))!=0 ){ if( (wsFlags & WHERE_AUTO_INDEX)!=0 ){ #ifndef SQLITE_OMIT_AUTOMATIC_INDEX @@ -6168,6 +6193,7 @@ void sqlite3WhereEnd(WhereInfo *pWInfo){ } assert( pWInfo->nLevel<=pTabList->nSrc ); + if( pWInfo->pExprMods ) whereUndoExprMods(pWInfo); for(i=0, pLevel=pWInfo->a; inLevel; i++, pLevel++){ int k, last; VdbeOp *pOp, *pLastOp; @@ -6304,7 +6330,6 @@ void sqlite3WhereEnd(WhereInfo *pWInfo){ /* Final cleanup */ - if( pWInfo->pExprMods ) whereUndoExprMods(pWInfo); pParse->nQueryLoop = pWInfo->savedNQueryLoop; whereInfoFree(db, pWInfo); return; diff --git a/src/whereInt.h b/src/whereInt.h index 93ab937c88..41417d2e7f 100644 --- a/src/whereInt.h +++ b/src/whereInt.h @@ -41,7 +41,7 @@ typedef struct WhereRightJoin WhereRightJoin; */ struct WhereMemBlock { WhereMemBlock *pNext; /* Next block in the chain */ - u8 sz; /* Bytes of space */ + u64 sz; /* Bytes of space */ }; /* diff --git a/src/wherecode.c b/src/wherecode.c index a942e6e7fb..3902c24f95 100644 --- a/src/wherecode.c +++ b/src/wherecode.c @@ -350,7 +350,7 @@ static void disableTerm(WhereLevel *pLevel, WhereTerm *pTerm){ int nLoop = 0; assert( pTerm!=0 ); while( (pTerm->wtFlags & TERM_CODED)==0 - && (pLevel->iLeftJoin==0 || ExprHasProperty(pTerm->pExpr, EP_FromJoin)) + && (pLevel->iLeftJoin==0 || ExprHasProperty(pTerm->pExpr, EP_OuterON)) && (pLevel->notReady & pTerm->prereqAll)==0 ){ if( nLoop && (pTerm->wtFlags & TERM_LIKE)!=0 ){ @@ -623,8 +623,7 @@ static int codeEqualityTerm( sqlite3ExprDelete(db, pX); }else{ aiMap = (int*)sqlite3DbMallocZero(pParse->db, sizeof(int)*nEq); - eType = sqlite3FindInIndex(pParse, pX, IN_INDEX_LOOP|IN_INDEX_REUSE_CUR, 0, aiMap,&iTab); - iTab = pExpr->iTable; + eType = sqlite3FindInIndex(pParse, pX, IN_INDEX_LOOP, 0, aiMap, &iTab); } pX = pExpr; } @@ -1072,7 +1071,7 @@ static void codeCursorHint( */ if( pTabItem->fg.jointype & JT_LEFT ){ Expr *pExpr = pTerm->pExpr; - if( !ExprHasProperty(pExpr, EP_FromJoin) + if( !ExprHasProperty(pExpr, EP_OuterON) || pExpr->w.iJoin!=pTabItem->iCursor ){ sWalker.eCode = 0; @@ -1081,7 +1080,7 @@ static void codeCursorHint( if( sWalker.eCode ) continue; } }else{ - if( ExprHasProperty(pTerm->pExpr, EP_FromJoin) ) continue; + if( ExprHasProperty(pTerm->pExpr, EP_OuterON) ) continue; } /* All terms in pWLoop->aLTerm[] except pEndRange are used to initialize @@ -2412,7 +2411,7 @@ Bitmask sqlite3WhereCodeOneLoopStart( Expr *pDelete; /* Local copy of OR clause term */ int jmp1 = 0; /* Address of jump operation */ testcase( (pTabItem[0].fg.jointype & JT_LEFT)!=0 - && !ExprHasProperty(pOrExpr, EP_FromJoin) + && !ExprHasProperty(pOrExpr, EP_OuterON) ); /* See TH3 vtab25.400 and ticket 614b25314c766238 */ pDelete = pOrExpr = sqlite3ExprDup(db, pOrExpr, 0); if( db->mallocFailed ){ @@ -2620,12 +2619,19 @@ Bitmask sqlite3WhereCodeOneLoopStart( } pE = pTerm->pExpr; assert( pE!=0 ); - if( (pTabItem->fg.jointype & (JT_LEFT|JT_LTORJ)) - && !ExprHasProperty(pE,EP_FromJoin|EP_InnerJoin) - ){ - continue; + if( pTabItem->fg.jointype & (JT_LEFT|JT_LTORJ|JT_RIGHT) ){ + if( !ExprHasProperty(pE,EP_OuterON|EP_InnerON) ){ + /* Defer processing WHERE clause constraints until after outer + ** join processing. tag-20220513a */ + continue; + }else{ + Bitmask m = sqlite3WhereGetMask(&pWInfo->sMaskSet, pE->w.iJoin); + if( m & pLevel->notReady ){ + /* An ON clause that is not ripe */ + continue; + } + } } - if( iLoop==1 && !sqlite3ExprCoveredByIndex(pE, pLevel->iTabCur, pIdx) ){ iNext = 2; continue; @@ -2684,7 +2690,7 @@ Bitmask sqlite3WhereCodeOneLoopStart( if( (pTerm->eOperator & (WO_EQ|WO_IS))==0 ) continue; if( (pTerm->eOperator & WO_EQUIV)==0 ) continue; if( pTerm->leftCursor!=iCur ) continue; - if( pTabItem->fg.jointype & (JT_LEFT|JT_LTORJ) ) continue; + if( pTabItem->fg.jointype & (JT_LEFT|JT_LTORJ|JT_RIGHT) ) continue; pE = pTerm->pExpr; #ifdef WHERETRACE_ENABLED /* 0x800 */ if( sqlite3WhereTrace & 0x800 ){ @@ -2692,7 +2698,7 @@ Bitmask sqlite3WhereCodeOneLoopStart( sqlite3WhereTermPrint(pTerm, pWC->nTerm-j); } #endif - assert( !ExprHasProperty(pE, EP_FromJoin) ); + assert( !ExprHasProperty(pE, EP_OuterON) ); assert( (pTerm->prereqRight & pLevel->notReady)!=0 ); assert( (pTerm->eOperator & (WO_OR|WO_AND))==0 ); pAlt = sqlite3WhereFindTerm(pWC, iCur, pTerm->u.x.leftColumn, notReady, @@ -2763,18 +2769,8 @@ Bitmask sqlite3WhereCodeOneLoopStart( pLevel->addrFirst = sqlite3VdbeCurrentAddr(v); sqlite3VdbeAddOp2(v, OP_Integer, 1, pLevel->iLeftJoin); VdbeComment((v, "record LEFT JOIN hit")); - for(pTerm=pWC->a, j=0; jnBase; j++, pTerm++){ - testcase( pTerm->wtFlags & TERM_VIRTUAL ); - testcase( pTerm->wtFlags & TERM_CODED ); - if( pTerm->wtFlags & (TERM_VIRTUAL|TERM_CODED) ) continue; - if( (pTerm->prereqAll & pLevel->notReady)!=0 ){ - assert( pWInfo->untestedTerms ); - continue; - } - if( pTabItem->fg.jointype & JT_LTORJ ) continue; - assert( pTerm->pExpr ); - sqlite3ExprIfFalse(pParse, pTerm->pExpr, addrCont, SQLITE_JUMPIFNULL); - pTerm->wtFlags |= TERM_CODED; + if( pLevel->pRJ==0 ){ + goto code_outer_join_constraints; /* WHERE clause constraints */ } } @@ -2790,6 +2786,26 @@ Bitmask sqlite3WhereCodeOneLoopStart( pRJ->addrSubrtn = sqlite3VdbeCurrentAddr(v); assert( pParse->withinRJSubrtn < 255 ); pParse->withinRJSubrtn++; + + /* WHERE clause constraints must be deferred until after outer join + ** row elimination has completed, since WHERE clause constraints apply + ** to the results of the OUTER JOIN. The following loop generates the + ** appropriate WHERE clause constraint checks. tag-20220513a. + */ + code_outer_join_constraints: + for(pTerm=pWC->a, j=0; jnBase; j++, pTerm++){ + testcase( pTerm->wtFlags & TERM_VIRTUAL ); + testcase( pTerm->wtFlags & TERM_CODED ); + if( pTerm->wtFlags & (TERM_VIRTUAL|TERM_CODED) ) continue; + if( (pTerm->prereqAll & pLevel->notReady)!=0 ){ + assert( pWInfo->untestedTerms ); + continue; + } + if( pTabItem->fg.jointype & JT_LTORJ ) continue; + assert( pTerm->pExpr ); + sqlite3ExprIfFalse(pParse, pTerm->pExpr, addrCont, SQLITE_JUMPIFNULL); + pTerm->wtFlags |= TERM_CODED; + } } #if WHERETRACE_ENABLED /* 0x20800 */ @@ -2845,7 +2861,7 @@ SQLITE_NOINLINE void sqlite3WhereRightJoinLoop( WhereTerm *pTerm = &pWC->a[k]; if( pTerm->wtFlags & TERM_VIRTUAL ) break; if( pTerm->prereqAll & ~mAll ) continue; - if( ExprHasProperty(pTerm->pExpr, EP_FromJoin|EP_InnerJoin) ) continue; + if( ExprHasProperty(pTerm->pExpr, EP_OuterON|EP_InnerON) ) continue; pSubWhere = sqlite3ExprAnd(pParse, pSubWhere, sqlite3ExprDup(pParse->db, pTerm->pExpr, 0)); } diff --git a/src/whereexpr.c b/src/whereexpr.c index 89c6a466ce..38e16b16b3 100644 --- a/src/whereexpr.c +++ b/src/whereexpr.c @@ -461,8 +461,8 @@ static int isAuxiliaryVtabOperator( ** a join, then transfer the appropriate markings over to derived. */ static void transferJoinMarkings(Expr *pDerived, Expr *pBase){ - if( pDerived ){ - pDerived->flags |= pBase->flags & EP_FromJoin; + if( pDerived && ExprHasProperty(pBase, EP_OuterON|EP_InnerON) ){ + pDerived->flags |= pBase->flags & (EP_OuterON|EP_InnerON); pDerived->w.iJoin = pBase->w.iJoin; } } @@ -917,7 +917,7 @@ static int termIsEquivalence(Parse *pParse, Expr *pExpr){ CollSeq *pColl; if( !OptimizationEnabled(pParse->db, SQLITE_Transitive) ) return 0; if( pExpr->op!=TK_EQ && pExpr->op!=TK_IS ) return 0; - if( ExprHasProperty(pExpr, EP_FromJoin) ) return 0; + if( ExprHasProperty(pExpr, EP_OuterON) ) return 0; aff1 = sqlite3ExprAffinity(pExpr->pLeft); aff2 = sqlite3ExprAffinity(pExpr->pRight); if( aff1!=aff2 @@ -1109,14 +1109,22 @@ static void exprAnalyze( } #endif - if( ExprHasProperty(pExpr, EP_FromJoin) ){ + if( ExprHasProperty(pExpr, EP_OuterON|EP_InnerON) ){ Bitmask x = sqlite3WhereGetMask(pMaskSet, pExpr->w.iJoin); - prereqAll |= x; - extraRight = x-1; /* ON clause terms may not be used with an index - ** on left table of a LEFT JOIN. Ticket #3015 */ - if( (prereqAll>>1)>=x ){ - sqlite3ErrorMsg(pParse, "ON clause references tables to its right"); - return; + if( ExprHasProperty(pExpr, EP_OuterON) ){ + prereqAll |= x; + extraRight = x-1; /* ON clause terms may not be used with an index + ** on left table of a LEFT JOIN. Ticket #3015 */ + if( (prereqAll>>1)>=x ){ + sqlite3ErrorMsg(pParse, "ON clause references tables to its right"); + return; + } + }else if( (prereqAll>>1)>=x ){ + /* The ON clause of an INNER JOIN references a table to its right. + ** Most other SQL database engines raise an error. But all versions + ** of SQLite going back to 3.0.0 have just put the ON clause constraint + ** into the WHERE clause and carried on. */ + ExprClearProperty(pExpr, EP_InnerON); } } pTerm->prereqAll = prereqAll; @@ -1184,7 +1192,7 @@ static void exprAnalyze( pNew->eOperator = (operatorMask(pDup->op) + eExtraOp) & opMask; }else if( op==TK_ISNULL - && !ExprHasProperty(pExpr,EP_FromJoin) + && !ExprHasProperty(pExpr,EP_OuterON) && 0==sqlite3ExprCanBeNull(pLeft) ){ assert( !ExprHasProperty(pExpr, EP_IntValue) ); @@ -1255,7 +1263,7 @@ static void exprAnalyze( else if( pExpr->op==TK_NOTNULL ){ if( pExpr->pLeft->op==TK_COLUMN && pExpr->pLeft->iColumn>=0 - && !ExprHasProperty(pExpr, EP_FromJoin) + && !ExprHasProperty(pExpr, EP_OuterON) ){ Expr *pNewExpr; Expr *pLeft = pExpr->pLeft; @@ -1459,8 +1467,8 @@ static void exprAnalyze( Expr *pNewExpr; pNewExpr = sqlite3PExpr(pParse, TK_MATCH, 0, sqlite3ExprDup(db, pRight, 0)); - if( ExprHasProperty(pExpr, EP_FromJoin) && pNewExpr ){ - ExprSetProperty(pNewExpr, EP_FromJoin); + if( ExprHasProperty(pExpr, EP_OuterON) && pNewExpr ){ + ExprSetProperty(pNewExpr, EP_OuterON); pNewExpr->w.iJoin = pExpr->w.iJoin; } idxNew = whereClauseInsert(pWC, pNewExpr, TERM_VIRTUAL|TERM_DYNAMIC); @@ -1827,9 +1835,9 @@ void sqlite3WhereTabFuncArgs( sqlite3ExprDup(pParse->db, pArgs->a[j].pExpr, 0), 0); pTerm = sqlite3PExpr(pParse, TK_EQ, pColRef, pRhs); if( pItem->fg.jointype & (JT_LEFT|JT_LTORJ) ){ - joinType = EP_FromJoin; + joinType = EP_OuterON; }else{ - joinType = EP_InnerJoin; + joinType = EP_InnerON; } sqlite3SetJoinExpr(pTerm, pItem->iCursor, joinType); whereClauseInsert(pWC, pTerm, TERM_DYNAMIC); diff --git a/test/altermalloc3.test b/test/altermalloc3.test index 7023b1c0f6..2dc0b46f29 100644 --- a/test/altermalloc3.test +++ b/test/altermalloc3.test @@ -21,6 +21,7 @@ ifcapable !altertable { return } + set ::TMPDBERROR [list 1 \ {unable to open a temporary database file for storing temporary tables} ] @@ -47,6 +48,7 @@ do_faultsim_test 1 -prep { faultsim_test_result {0 {}} $::TMPDBERROR } + #------------------------------------------------------------------------- # dbsqlfuzz e3dd84cda3848016a6a6024c7249d09bc2ef2615 # @@ -59,6 +61,11 @@ do_execsql_test 2.0 { SELECT a FROM cte1 ), 1); END; + + CREATE TRIGGER r1 AFTER INSERT ON t2 BEGIN + UPDATE t2 SET k=1 FROM t2 AS one, t2 AS two NATURAL JOIN t2 AS three + WHERE one.k=two.v; + END; } faultsim_save_and_close diff --git a/test/altertab3.test b/test/altertab3.test index c786570451..5fd17f3a2f 100644 --- a/test/altertab3.test +++ b/test/altertab3.test @@ -645,7 +645,7 @@ do_execsql_test 26.5 { } {} do_catchsql_test 26.6 { ALTER TABLE t1 RENAME TO t2; -} {1 {error in trigger xx: ambiguous column name: xx}} +} {1 {error in trigger xx: no such column: xx}} #------------------------------------------------------------------------- diff --git a/test/altertrig.test b/test/altertrig.test new file mode 100644 index 0000000000..934a636669 --- /dev/null +++ b/test/altertrig.test @@ -0,0 +1,163 @@ +# 2022 May 27 +# +# 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. +# +#************************************************************************* +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix altertrig + +# If SQLITE_OMIT_ALTERTABLE is defined, omit this file. +ifcapable !altertable { + finish_test + return +} + +proc collapse_whitespace {in} { + regsub -all {[ \t\n]+} [string trim $in] { } +} + +proc do_whitespace_sql_test {tn sql res} { + set got [execsql $sql] + set wgot [list] + set wres [list] + foreach g $got { lappend wgot [collapse_whitespace $g] } + foreach r $res { lappend wres [collapse_whitespace $r] } + + uplevel [list do_test $tn [list set {} $wgot] $wres] +} + +do_execsql_test 1.0 { + CREATE TABLE t1(x); + CREATE TABLE t2(y); + CREATE TABLE t3(z); + CREATE TABLE t4(a); + + CREATE TRIGGER r1 INSERT ON t1 BEGIN + UPDATE t1 SET d='xyz' FROM t2, t3; + END; +} + +do_whitespace_sql_test 1.1 { + ALTER TABLE t3 RENAME TO t5; + SELECT sql FROM sqlite_schema WHERE type='trigger'; +} {{ + CREATE TRIGGER r1 INSERT ON t1 BEGIN + UPDATE t1 SET d='xyz' FROM t2, "t5"; + END +}} + +do_execsql_test 1.2 { + DROP TRIGGER r1; + CREATE TRIGGER r1 INSERT ON t1 BEGIN + UPDATE t1 SET d='xyz' FROM t2, (SELECT * FROM t5); + END; +} + +do_whitespace_sql_test 1.3 { + ALTER TABLE t5 RENAME TO t3; + SELECT sql FROM sqlite_schema WHERE type='trigger'; +} {{ + CREATE TRIGGER r1 INSERT ON t1 BEGIN + UPDATE t1 SET d='xyz' FROM t2, (SELECT * FROM "t3"); + END +}} + +foreach {tn alter update final} { + 1 { + ALTER TABLE t3 RENAME TO t10 + } { + UPDATE t1 SET d='xyz' FROM t2, (SELECT * FROM t3) + } { + UPDATE t1 SET d='xyz' FROM t2, (SELECT * FROM "t10") + } + + 2 { + ALTER TABLE t3 RENAME TO t10 + } { + UPDATE t1 SET a='xyz' FROM t3, (SELECT * FROM (SELECT e FROM t3)) + } { + UPDATE t1 SET a='xyz' FROM "t10", (SELECT * FROM (SELECT e FROM "t10")) + } + + 3 { + ALTER TABLE t3 RENAME e TO abc + } { + UPDATE t1 SET a='xyz' FROM t3, (SELECT * FROM (SELECT e FROM t3)) + } { + UPDATE t1 SET a='xyz' FROM t3, (SELECT * FROM (SELECT abc FROM t3)) + } + + 4 { + ALTER TABLE t2 RENAME c TO abc + } { + UPDATE t1 SET a='xyz' FROM t3, (SELECT 1 FROM t2 WHERE c) + } { + UPDATE t1 SET a='xyz' FROM t3, (SELECT 1 FROM t2 WHERE abc) + } + + 5 { + ALTER TABLE t2 RENAME c TO abc + } { + UPDATE t1 SET a=t2.c FROM t2 + } { + UPDATE t1 SET a=t2.abc FROM t2 + } + + 6 { + ALTER TABLE t2 RENAME c TO abc + } { + UPDATE t1 SET a=t2.c FROM t2, t3 + } { + UPDATE t1 SET a=t2.abc FROM t2, t3 + } + + 7 { + ALTER TABLE t4 RENAME e TO abc + } { + UPDATE t1 SET a=1 FROM t3 NATURAL JOIN t4 WHERE t4.e=a + } { + UPDATE t1 SET a=1 FROM t3 NATURAL JOIN t4 WHERE t4.abc=a + } + + 8 { + ALTER TABLE t4 RENAME TO abc + } { + UPDATE t1 SET a=1 FROM t3 NATURAL JOIN t4 WHERE t4.e=a + } { + UPDATE t1 SET a=1 FROM t3 NATURAL JOIN "abc" WHERE "abc".e=a + } + +} { + reset_db + do_execsql_test 2.$tn.1 { + CREATE TABLE t1(a,b); + CREATE TABLE t2(c,d); + CREATE TABLE t3(e,f); + CREATE TABLE t4(e,f); + } + do_execsql_test 2.$tn.2 " + CREATE TRIGGER r1 INSERT ON t1 BEGIN + $update; + END + " + do_execsql_test 2.$tn.3 $alter + + do_whitespace_sql_test 2.$tn.4 { + SELECT sqL FROM sqlite_schema WHERE type='trigger' + } "{ + CREATE TRIGGER r1 INSERT ON t1 BEGIN + $final; + END + }" +} + +finish_test + diff --git a/test/e_select.test b/test/e_select.test index 1ffd4021b6..5a3f0d30dc 100644 --- a/test/e_select.test +++ b/test/e_select.test @@ -618,11 +618,12 @@ foreach {tn select res} { } { do_join_test e_select-1.7.$tn $select $res } -# EVIDENCE-OF: R-42531-52874 If the join-operator is a "LEFT JOIN" or + +# EVIDENCE-OF: R-24610-05866 If the join-operator is a "LEFT JOIN" or # "LEFT OUTER JOIN", then after the ON or USING filtering clauses have # been applied, an extra row is added to the output for each row in the -# original left-hand input dataset that corresponds to no rows at all in -# the composite dataset (if any). +# original left-hand input dataset that does not match any row in the +# right-hand dataset. # do_execsql_test e_select-1.8.0 { CREATE TABLE t7(a, b, c); diff --git a/test/expr.test b/test/expr.test index c64b6cb706..55b1dc9502 100644 --- a/test/expr.test +++ b/test/expr.test @@ -181,29 +181,53 @@ if {[working_64bit_int]} { } test_expr expr-1.111 {i1=NULL, i2=8} {i1 IS i2} 0 +test_expr expr-1.111b {i1=NULL, i2=8} {i1 IS NOT DISTINCT FROM i2} 0 test_expr expr-1.112 {i1=NULL, i2=NULL} {i1 IS i2} 1 +test_expr expr-1.112b {i1=NULL, i2=NULL} {i1 IS NOT DISTINCT FROM i2} 1 test_expr expr-1.113 {i1=6, i2=NULL} {i1 IS i2} 0 +test_expr expr-1.113b {i1=6, i2=NULL} {i1 IS NOT DISTINCT FROM i2} 0 test_expr expr-1.114 {i1=6, i2=6} {i1 IS i2} 1 +test_expr expr-1.114b {i1=6, i2=6} {i1 IS NOT DISTINCT FROM i2} 1 test_expr expr-1.115 {i1=NULL, i2=8} \ {CASE WHEN i1 IS i2 THEN 'yes' ELSE 'no' END} no +test_expr expr-1.115b {i1=NULL, i2=8} \ + {CASE WHEN i1 IS NOT DISTINCT FROM i2 THEN 'yes' ELSE 'no' END} no test_expr expr-1.116 {i1=NULL, i2=NULL} \ {CASE WHEN i1 IS i2 THEN 'yes' ELSE 'no' END} yes +test_expr expr-1.116b {i1=NULL, i2=NULL} \ + {CASE WHEN i1 IS NOT DISTINCT FROM i2 THEN 'yes' ELSE 'no' END} yes test_expr expr-1.117 {i1=6, i2=NULL} \ {CASE WHEN i1 IS i2 THEN 'yes' ELSE 'no' END} no +test_expr expr-1.117b {i1=6, i2=NULL} \ + {CASE WHEN i1 IS NOT DISTINCT FROM i2 THEN 'yes' ELSE 'no' END} no test_expr expr-1.118 {i1=8, i2=8} \ {CASE WHEN i1 IS i2 THEN 'yes' ELSE 'no' END} yes +test_expr expr-1.118b {i1=8, i2=8} \ + {CASE WHEN i1 IS NOT DISTINCT FROM i2 THEN 'yes' ELSE 'no' END} yes test_expr expr-1.119 {i1=NULL, i2=8} {i1 IS NOT i2} 1 +test_expr expr-1.119b {i1=NULL, i2=8} {i1 IS DISTINCT FROM i2} 1 test_expr expr-1.120 {i1=NULL, i2=NULL} {i1 IS NOT i2} 0 +test_expr expr-1.120b {i1=NULL, i2=NULL} {i1 IS DISTINCT FROM i2} 0 test_expr expr-1.121 {i1=6, i2=NULL} {i1 IS NOT i2} 1 +test_expr expr-1.121b {i1=6, i2=NULL} {i1 IS DISTINCT FROM i2} 1 test_expr expr-1.122 {i1=6, i2=6} {i1 IS NOT i2} 0 +test_expr expr-1.122b {i1=6, i2=6} {i1 IS DISTINCT FROM i2} 0 test_expr expr-1.123 {i1=NULL, i2=8} \ {CASE WHEN i1 IS NOT i2 THEN 'yes' ELSE 'no' END} yes +test_expr expr-1.123b {i1=NULL, i2=8} \ + {CASE WHEN i1 IS DISTINCT FROM i2 THEN 'yes' ELSE 'no' END} yes test_expr expr-1.124 {i1=NULL, i2=NULL} \ {CASE WHEN i1 IS NOT i2 THEN 'yes' ELSE 'no' END} no +test_expr expr-1.124b {i1=NULL, i2=NULL} \ + {CASE WHEN i1 IS DISTINCT FROM i2 THEN 'yes' ELSE 'no' END} no test_expr expr-1.125 {i1=6, i2=NULL} \ {CASE WHEN i1 IS NOT i2 THEN 'yes' ELSE 'no' END} yes +test_expr expr-1.125b {i1=6, i2=NULL} \ + {CASE WHEN i1 IS DISTINCT FROM i2 THEN 'yes' ELSE 'no' END} yes test_expr expr-1.126 {i1=8, i2=8} \ {CASE WHEN i1 IS NOT i2 THEN 'yes' ELSE 'no' END} no +test_expr expr-1.126b {i1=8, i2=8} \ + {CASE WHEN i1 IS DISTINCT FROM i2 THEN 'yes' ELSE 'no' END} no do_catchsql_test expr-1.127 { SELECT 1 IS #1; diff --git a/test/func7.test b/test/func7.test index c8ae2931e1..bb4f80b325 100644 --- a/test/func7.test +++ b/test/func7.test @@ -60,6 +60,12 @@ do_execsql_test func7-pg-170 { do_execsql_test func7-pg-180 { SELECT log10(1000.0) } {3.0} +do_execsql_test func7-pg-181 { + SELECT format('%.30f', log10(100.0) ); +} {2.000000000000000000000000000000} +do_execsql_test func7-pg-182 { + SELECT format('%.30f', ln(exp(2.0)) ); +} {2.000000000000000000000000000000} do_execsql_test func7-pg-190 { SELECT log(2.0, 64.0) } {6.0} diff --git a/test/in.test b/test/in.test index b0eb371cb7..7c4cc51878 100644 --- a/test/in.test +++ b/test/in.test @@ -798,4 +798,13 @@ do_execsql_test in-20.1 { SELECT (1 IN (2 IS TRUE)); } {1} +# Forum post: https://sqlite.org/forum/forumpost/5782619992. +# +reset_db +do_execsql_test in-21.1 { + CREATE TABLE t0(c0); + SELECT COUNT(*) FROM t0 ORDER BY (t0.c0 IN ()); +} {0} + + finish_test diff --git a/test/join.test b/test/join.test index 38cfb74569..09ede1a4af 100644 --- a/test/join.test +++ b/test/join.test @@ -250,6 +250,19 @@ do_test join-2.1 { } } {1 2 3 4 2 3 4 5 3 4 5 {}} +# EVIDENCE-OF: R-52129-05406 you can say things like "OUTER LEFT NATURAL +# JOIN" which means the same as "NATURAL LEFT OUTER JOIN". +do_test join-2.1b { + execsql { + SELECT * FROM t1 OUTER LEFT NATURAL JOIN t2; + } +} {1 2 3 4 2 3 4 5 3 4 5 {}} +do_test join-2.1c { + execsql { + SELECT * FROM t1 NATURAL LEFT OUTER JOIN t2; + } +} {1 2 3 4 2 3 4 5 3 4 5 {}} + # ticket #3522 do_test join-2.1.1 { execsql2 { @@ -328,6 +341,9 @@ do_test join-3.6 { SELECT * FROM t1 JOIN t2 ON t3.a=t2.b; } } {1 {no such column: t3.a}} + +# EVIDENCE-OF: R-47973-48020 you cannot say "INNER OUTER JOIN", because +# that would be contradictory. do_test join-3.7 { catchsql { SELECT * FROM t1 INNER OUTER JOIN t2; diff --git a/test/join8.test b/test/join8.test index 3a3b06a1c4..cccb167ac1 100644 --- a/test/join8.test +++ b/test/join8.test @@ -20,6 +20,8 @@ ifcapable !vtab { } db null NULL +# EVIDENCE-OF: R-33754-02880 you can say "LEFT RIGHT JOIN" which is the +# same as "FULL JOIN". do_execsql_test join8-10 { CREATE TABLE t1(a,b,c); CREATE TABLE t2(x,y); @@ -226,6 +228,27 @@ do_execsql_test join8-7010 { - - - - - - - - 305 5 - - - - - - - - 310 10 } + +# EVIDENCE-OF: R-33754-02880 you can say "LEFT RIGHT JOIN" which is the +# same as "FULL JOIN". +do_execsql_test join8-7011 { + WITH t0 AS MATERIALIZED ( + SELECT t1.*, t2.*, t3.* + FROM t1 INNER JOIN t2 ON t1.b=t2.b AND t2.x>0 + RIGHT JOIN t3 ON t1.c=t3.c AND t3.y>0 + ) + SELECT * FROM t0 LEFT RIGHT JOIN t4 ON t0.a=t4.d AND t4.z>0 + ORDER BY coalesce(t0.a, t0.y+200, t4.d); +} { + 6 106 206 306 106 6 206 6 - - + - - - - - - 200 0 - - + - - - - - - 203 3 - - + - - - - - - 209 9 - - + - - - - - - - - 300 0 + - - - - - - - - 305 5 + - - - - - - - - 310 10 +} + do_execsql_test join8-7020 { EXPLAIN QUERY PLAN WITH t0 AS MATERIALIZED ( @@ -237,4 +260,189 @@ do_execsql_test join8-7020 { ORDER BY coalesce(t0.a, t0.y+200, t4.d); } {/.*BLOOM FILTER ON t2.*BLOOM FILTER ON t3.*BLOOM FILTER ON t4.*/} +# 2022-05-12 Difference with PG found (by Dan) while exploring +# https://sqlite.org/forum/forumpost/677a0ab93fcd9ccd +# +reset_db +do_execsql_test join8-8000 { + CREATE TABLE t1(a INT, b INT); + CREATE TABLE t2(c INT, d INT); + CREATE TABLE t3(e INT, f INT); + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t2 VALUES(3, 4); + INSERT INTO t3 VALUES(5, 6); +} {} +do_execsql_test join8-8010 { + SELECT * + FROM t3 LEFT JOIN t2 ON true + JOIN t1 ON (t3.e IS t2.c); +} {} +do_execsql_test join8-8020 { + SELECT * + FROM t3 LEFT JOIN t2 ON true + JOIN t1 ON (t3.e IS NOT DISTINCT FROM t2.c); +} {} + +# 2022-05-13 The idea of reusing subquery cursors does not +# work, if the cursors are used both for scanning and lookups. +# +reset_db +db null - +do_execsql_test join8-9000 { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b TEXT, c TEXT, d REAL); + INSERT INTO t1 VALUES(1,'E','bb',NULL),(2,NULL,NULL,NULL); + SELECT * FROM t1 NATURAL RIGHT JOIN t1 AS t2 WHERE (a,b) IN (SELECT a+0, b FROM t1); +} {1 E bb -} + +# 2022-05-14 https://sqlite.org/forum/forumpost/c06b10ad7e +# +reset_db +db null - +do_execsql_test join8-10000 { + CREATE TABLE t1(c0 INT UNIQUE); + CREATE TABLE t2(c0); + CREATE TABLE t2i(c0 INT); + CREATE TABLE t3(c0 INT); + INSERT INTO t1 VALUES(1); + INSERT INTO t2 VALUES(2); + INSERT INTO t2i VALUES(2); + INSERT INTO t3 VALUES(3); +} {} +do_execsql_test join8-10010 { + SELECT DISTINCT t1.c0, t3.c0 + FROM t2 NATURAL JOIN t1 RIGHT JOIN t3 ON t1.c0; +} {- 3} +do_execsql_test join8-10020 { + SELECT t1.c0, t3.c0 + FROM t2 NATURAL JOIN t1 RIGHT JOIN t3 ON t1.c0; +} {- 3} +do_execsql_test join8-10030 { + SELECT DISTINCT t1.c0, t3.c0 + FROM t2 NATURAL CROSS JOIN t1 RIGHT JOIN t3 ON t1.c0; +} {- 3} +do_execsql_test join8-10040 { + SELECT t1.c0, t3.c0 + FROM t1 NATURAL CROSS JOIN t2 RIGHT JOIN t3 ON t1.c0; +} {- 3} +do_execsql_test join8-10050 { + SELECT DISTINCT t1.c0, t3.c0 + FROM t2i NATURAL JOIN t1 RIGHT JOIN t3 ON t1.c0; +} {- 3} +do_execsql_test join8-10060 { + SELECT DISTINCT +t1.c0, t3.c0 + FROM t2 NATURAL JOIN t1 RIGHT JOIN t3 ON t1.c0; +} {- 3} +do_execsql_test join8-10070 { + SELECT DISTINCT +t1.c0, t3.c0 + FROM t1 NATURAL CROSS JOIN t2 RIGHT JOIN t3 ON t1.c0; +} {- 3} +do_execsql_test join8-10080 { + SELECT DISTINCT t1.c0, t3.c0 + FROM t2 NATURAL JOIN t1 RIGHT JOIN t3 ON t1.c0<>0; +} {- 3} + +# 2022-05-14 +# index-on-expr scan on a RIGHT JOIN +# dbsqlfuzz 39ee60004ff027a9e2846cf76e02cd5ac0953739 +# +reset_db +db null - +do_execsql_test join8-11000 { + CREATE TABLE t1(a); + CREATE TABLE t2(b); + INSERT INTO t2 VALUES(0),(1),(2); + SELECT * FROM t1 RIGHT JOIN t2 ON (a=b) WHERE 99+(b+1)!=99; +} {- 0 - 1 - 2} +do_execsql_test join8-11010 { + CREATE INDEX t2b ON t2(b+1) WHERE b IS NOT NULL; + SELECT * FROM t1 RIGHT JOIN t2 ON (a=b) WHERE 99+(b+1)!=99; +} {- 0 - 1 - 2} +do_execsql_test join8-11020 { + DROP TABLE t1; + DROP TABLE t2; + CREATE TABLE t1(a); + CREATE TABLE t2(b, c, d); + INSERT INTO t2 VALUES(1, 3, 'not-4'); + SELECT b, d FROM t1 RIGHT JOIN t2 WHERE (b+0)=1 AND d!=4; +} {1 not-4} +do_execsql_test join8-11030 { + CREATE INDEX i2 ON t2((b+0), d); + SELECT b, d FROM t1 RIGHT JOIN t2 WHERE (b+0)=1 AND d!=4; +} {1 not-4} +do_execsql_test join8-11040 { + DROP INDEX i2; + CREATE INDEX i2 ON t2((b+0), d) WHERE d IS NOT NULL; + SELECT b, d FROM t1 RIGHT JOIN t2 WHERE (b+0)=1 AND d!=4; +} {1 not-4} + +# 2022-05-23 +# NATURAL JOIN name resolution is more forgiving with LEFT JOIN +# https://sqlite.org/forum/forumpost/e90a8e6e6f +# +reset_db +db null - +do_execsql_test join8-12000 { + CREATE TABLE t1(a INT); INSERT INTO t1 VALUES(0),(1); + CREATE TABLE t2(a INT); INSERT INTO t2 VALUES(0),(2); + CREATE TABLE t3(a INT); INSERT INTO t3 VALUES(0),(3); +} {} +do_catchsql_test join8-12010 { + SELECT * FROM t1 RIGHT JOIN t2 ON t2.a<>0 NATURAL RIGHT JOIN t3; +} {1 {ambiguous reference to a in USING()}} +do_catchsql_test join8-12020 { + SELECT * FROM t1 RIGHT JOIN t2 ON t2.a<>0 NATURAL LEFT JOIN t3; +} {1 {ambiguous reference to a in USING()}} +do_catchsql_test join8-12030 { + SELECT * FROM t1 LEFT JOIN t2 ON t2.a<>0 NATURAL RIGHT JOIN t3; +} {1 {ambiguous reference to a in USING()}} + +# The following query should probably also return the same error as the +# previous three cases. However, historical versions of SQLite have always +# let it pass. We will not "fix" this, since to do so might break legacy +# applications. +# +do_catchsql_test join8-12040 { + SELECT * FROM t1 LEFT JOIN t2 ON t2.a<>0 NATURAL LEFT JOIN t3; +} {0 {0 2 1 2}} + +# 2022-05-24 +# https://sqlite.org/forum/forumpost/687b0bf563a1d4f1 +# +reset_db +do_execsql_test join8-13000 { + CREATE TABLE t0(t TEXT, u TEXT); INSERT INTO t0 VALUES('t', 'u'); + CREATE TABLE t1(v TEXT, w TEXT); INSERT INTO t1 VALUES('v', 'w'); + CREATE TABLE t2(x TEXT, y TEXT); INSERT INTO t2 VALUES('x', 'y'); + SELECT * FROM t0 JOIN t1 ON (t2.x NOTNULL) LEFT JOIN t2 ON false; + SELECT * FROM t0 JOIN t1 ON (t2.x NOTNULL) LEFT JOIN t2 ON false + WHERE t2.y ISNULL; +} {} + +# 2022-05-25 +# https://sqlite.org/forum/forumpost/5cfe08eed6 +# +reset_db +do_execsql_test join8-14000 { + CREATE TABLE t0(a TEXT, b TEXT, c TEXT); + CREATE TABLE t1(a TEXT); + INSERT INTO t1 VALUES('1'); + CREATE VIEW v0 AS SELECT 'xyz' AS d; + SELECT * FROM v0 RIGHT JOIN t1 ON t1.a<>'' INNER JOIN t0 ON t0.c<>''; + SELECT * FROM v0 RIGHT JOIN t1 ON t1.a<>'' INNER JOIN t0 ON t0.c<>'' WHERE b ISNULL; +} {} +do_execsql_test join8-14010 { + CREATE TABLE y0(a INT); + CREATE TABLE y1(b INT); INSERT INTO y1 VALUES(1), (2); + CREATE TABLE y2(c INT); INSERT INTO y2 VALUES(3), (4); +} {} +db null - +do_execsql_test join8-14020 { + SELECT * FROM y0 RIGHT JOIN y1 ON true INNER JOIN y2 ON true WHERE y2.c!=99 AND y2.c!=98; +} { + - 1 3 + - 1 4 + - 2 3 + - 2 4 +} + finish_test diff --git a/test/joinE.test b/test/joinE.test new file mode 100644 index 0000000000..8c8e72ab2d --- /dev/null +++ b/test/joinE.test @@ -0,0 +1,443 @@ +# 2022-05-13 +# +# 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 implements tests for JOINs that use Bloom filters. +# +# The test case output is (mostly) all generated by PostgreSQL 14. This +# test module was created as follows: +# +# 1. Run a TCL script (included at the bottom of this file) that +# generates an input script for "psql" that will run man +# diverse tests on joins. +# +# 2. Run the script from step (1) through psql and collect the +# output. +# +# 3. Make a few minor global search-and-replace operations to convert +# the psql output into a form suitable for this test module. +# +# 4. Add this header, and the script content at the footer. +# +set testdir [file dirname $argv0] +source $testdir/tester.tcl +db nullvalue - +db eval { + CREATE TABLE t1(a INT); + INSERT INTO t1 VALUES(1),(NULL); + CREATE TABLE t2(b INT); + INSERT INTO t2 VALUES(2),(NULL); +} +do_execsql_test joinE-1 { + SELECT a, b + FROM t1 INNER JOIN t2 ON true + ORDER BY coalesce(a,b,3); +} { + 1 2 + 1 - + - 2 + - - +} +do_execsql_test joinE-2 { + SELECT a, b + FROM t1 INNER JOIN t2 ON true WHERE a IS NULL + ORDER BY coalesce(a,b,3); +} { + - 2 + - - +} +do_execsql_test joinE-3 { + SELECT a, b + FROM t1 INNER JOIN t2 ON a IS NULL + ORDER BY coalesce(a,b,3); +} { + - 2 + - - +} +do_execsql_test joinE-4 { + SELECT a, b + FROM t1 INNER JOIN t2 ON true WHERE b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - + - - +} +do_execsql_test joinE-5 { + SELECT a, b + FROM t1 INNER JOIN t2 ON b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - + - - +} +do_execsql_test joinE-6 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON true + ORDER BY coalesce(a,b,3); +} { + 1 2 + 1 - + - 2 + - - +} +do_execsql_test joinE-7 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON true WHERE a IS NULL + ORDER BY coalesce(a,b,3); +} { + - 2 + - - +} +do_execsql_test joinE-8 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON a IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - + - 2 + - - +} +do_execsql_test joinE-9 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON true WHERE b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - + - - +} +do_execsql_test joinE-10 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - + - - +} +do_execsql_test joinE-11 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON true + ORDER BY coalesce(a,b,3); +} { + 1 2 + 1 - + - 2 + - - +} +do_execsql_test joinE-12 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON true WHERE a IS NULL + ORDER BY coalesce(a,b,3); +} { + - 2 + - - +} +do_execsql_test joinE-13 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON a IS NULL + ORDER BY coalesce(a,b,3); +} { + - 2 + - - +} +do_execsql_test joinE-14 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON true WHERE b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - + - - +} +do_execsql_test joinE-15 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - + - 2 + - - +} +do_execsql_test joinE-16 { + SELECT a, b + FROM t1 FULL JOIN t2 ON true + ORDER BY coalesce(a,b,3); +} { + 1 2 + 1 - + - 2 + - - +} +do_execsql_test joinE-17 { + SELECT a, b + FROM t1 FULL JOIN t2 ON true WHERE a IS NULL + ORDER BY coalesce(a,b,3); +} { + - 2 + - - +} + +# PG-14 is unable to perform this join. It says: FULL JOIN is only +# supported with merge-joinable or hash-joinable join conditions +# +# do_execsql_test joinE-18 { +# SELECT a, b +# FROM t1 FULL JOIN t2 ON a IS NULL +# ORDER BY coalesce(a,b,3); +# } { +# } + +do_execsql_test joinE-19 { + SELECT a, b + FROM t1 FULL JOIN t2 ON true WHERE b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - + - - +} + +# PG-14 is unable to perform this join. It says: FULL JOIN is only +# supported with merge-joinable or hash-joinable join conditions +# +# do_execsql_test joinE-20 { +# SELECT a, b +# FROM t1 FULL JOIN t2 ON b IS NULL +# ORDER BY coalesce(a,b,3); +# } { +# } + +db eval { + DELETE FROM t1; + INSERT INTO t1 VALUES(1); + DELETE FROM t2; + INSERT INTO t2 VALUES(NULL); +} + +do_execsql_test joinE-21 { + SELECT a, b + FROM t1 INNER JOIN t2 ON true + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-22 { + SELECT a, b + FROM t1 INNER JOIN t2 ON true WHERE a IS NULL + ORDER BY coalesce(a,b,3); +} { +} +do_execsql_test joinE-23 { + SELECT a, b + FROM t1 INNER JOIN t2 ON a IS NULL + ORDER BY coalesce(a,b,3); +} { +} +do_execsql_test joinE-24 { + SELECT a, b + FROM t1 INNER JOIN t2 ON true WHERE b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-25 { + SELECT a, b + FROM t1 INNER JOIN t2 ON b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-26 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON true + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-27 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON true WHERE a IS NULL + ORDER BY coalesce(a,b,3); +} { +} +do_execsql_test joinE-28 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON a IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-29 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON true WHERE b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-30 { + SELECT a, b + FROM t1 LEFT JOIN t2 ON b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-31 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON true + ORDER BY coalesce(a,b,3); +} { + 1 - +} + +do_execsql_test joinE-32 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON true WHERE a IS NULL + ORDER BY coalesce(a,b,3); +} { +} + +do_execsql_test joinE-33 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON a IS NULL + ORDER BY coalesce(a,b,3); +} { + - - +} +do_execsql_test joinE-34 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON true WHERE b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-35 { + SELECT a, b + FROM t1 RIGHT JOIN t2 ON b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-36 { + SELECT a, b + FROM t1 FULL JOIN t2 ON true + ORDER BY coalesce(a,b,3); +} { + 1 - +} +do_execsql_test joinE-37 { + SELECT a, b + FROM t1 FULL JOIN t2 ON true WHERE a IS NULL + ORDER BY coalesce(a,b,3); +} { +} + +# PG-14 is unable +# +# do_execsql_test joinE-38 { +# SELECT a, b +# FROM t1 FULL JOIN t2 ON a IS NULL +# ORDER BY coalesce(a,b,3); +# } { +# } + +do_execsql_test joinE-39 { + SELECT a, b + FROM t1 FULL JOIN t2 ON true WHERE b IS NULL + ORDER BY coalesce(a,b,3); +} { + 1 - +} + +# PG-14 is unable +# do_execsql_test joinE-40 { +# SELECT a, b +# FROM t1 FULL JOIN t2 ON b IS NULL +# ORDER BY coalesce(a,b,3); +# } { +# } + +finish_test + +############################################################################## +# This is the PG-14 test script generator +# +# puts " +# \\pset border off +# \\pset tuples_only on +# \\pset null - +# +# DROP TABLE IF EXISTS t1; +# DROP TABLE IF EXISTS t2; +# CREATE TABLE t1(a INT); +# INSERT INTO t1 VALUES(1),(NULL); +# CREATE TABLE t2(b INT); +# INSERT INTO t2 VALUES(2),(NULL); +# " +# +# proc echo {prefix txt} { +# regsub -all {\n} $txt \n$prefix txt +# puts "$prefix$txt" +# } +# +# set n 0 +# set k 0 +# foreach j1 {INNER LEFT RIGHT FULL} { +# foreach on1 { +# true +# {true WHERE a IS NULL} +# {a IS NULL} +# {true WHERE b IS NULL} +# {b IS NULL} +# } { +# +# incr n +# incr k +# set q1 "" +# append q1 "SELECT a, b\n" +# append q1 " FROM t1 $j1 JOIN t2 ON $on1\n" +# append q1 " ORDER BY coalesce(a,b,3);" +# +# echo "\\qecho " "do_execsql_test joinE-$n \{" +# echo "\\qecho X " $q1 +# echo "\\qecho " "\} \{" +# puts $q1 +# echo "\\qecho " "\}" +# +# } +# } +# +# puts " +# DELETE FROM t1; +# INSERT INTO t1 VALUES(1); +# DELETE FROM t2; +# INSERT INTO t2 VALUES(NULL); +# " +# +# foreach j1 {INNER LEFT RIGHT FULL} { +# foreach on1 { +# true +# {true WHERE a IS NULL} +# {a IS NULL} +# {true WHERE b IS NULL} +# {b IS NULL} +# } { +# +# incr n +# incr k +# set q1 "" +# append q1 "SELECT a, b\n" +# append q1 " FROM t1 $j1 JOIN t2 ON $on1\n" +# append q1 " ORDER BY coalesce(a,b,3);" +# +# echo "\\qecho " "do_execsql_test joinE-$n \{" +# echo "\\qecho X " $q1 +# echo "\\qecho " "\} \{" +# puts $q1 +# echo "\\qecho " "\}" +# +# } +# } diff --git a/test/permutations.test b/test/permutations.test index a7a1fd7d43..95688d2a08 100644 --- a/test/permutations.test +++ b/test/permutations.test @@ -217,7 +217,8 @@ test_suite "valgrind" -prefix "" -description { fail under valgrind) omitted. } -files [ test_set $allquicktests -exclude *malloc* *ioerr* *fault* *_err* wal.test \ - shell*.test crash8.test atof1.test selectG.test \ + shell2.test shell6.test shell7.test \ + crash8.test atof1.test selectG.test \ tkt-fc62af4523.test numindex1.test corruptK.test ] -initialize { set ::G(valgrind) 1 diff --git a/test/shell1.test b/test/shell1.test index 3c7061c55d..26426535b7 100644 --- a/test/shell1.test +++ b/test/shell1.test @@ -23,7 +23,7 @@ # set testdir [file dirname $argv0] source $testdir/tester.tcl -set CLI [test_find_cli] +set CLI [test_cli_invocation] db close forcedelete test.db test.db-journal test.db-wal sqlite3 db test.db diff --git a/test/shell2.test b/test/shell2.test index 29712c7f07..2237404e5a 100644 --- a/test/shell2.test +++ b/test/shell2.test @@ -136,16 +136,13 @@ SELECT * FROM foo1; SELECT * FROM foo2; INSERT INTO foo1(a) VALUES(1); CREATE TABLE foo2(b); INSERT INTO foo2(b) VALUES(1); -SELECT * FROM foo1; +SELECT * FROM foo1; SELECT * FROM foo2; 1 -SELECT * FROM foo2; 1 -INSERT INTO foo1(a) VALUES(2); -INSERT INTO foo2(b) VALUES(2); -SELECT * FROM foo1; +INSERT INTO foo1(a) VALUES(2); INSERT INTO foo2(b) VALUES(2); +SELECT * FROM foo1; SELECT * FROM foo2; 1 2 -SELECT * FROM foo2; 1 2 }} @@ -170,19 +167,16 @@ CREATE TABLE foo1(a); INSERT INTO foo1(a) VALUES(1); CREATE TABLE foo2(b); INSERT INTO foo2(b) VALUES(1); -SELECT * FROM foo1; +SELECT * FROM foo1; SELECT * FROM foo2; a 1 -SELECT * FROM foo2; b 1 -INSERT INTO foo1(a) VALUES(2); -INSERT INTO foo2(b) VALUES(2); -SELECT * FROM foo1; +INSERT INTO foo1(a) VALUES(2); INSERT INTO foo2(b) VALUES(2); +SELECT * FROM foo1; SELECT * FROM foo2; a 1 2 -SELECT * FROM foo2; b 1 2 diff --git a/test/shell3.test b/test/shell3.test index e5a0c124e0..f8d69946e7 100644 --- a/test/shell3.test +++ b/test/shell3.test @@ -22,7 +22,7 @@ # set testdir [file dirname $argv0] source $testdir/tester.tcl -set CLI [test_find_cli] +set CLI [test_cli_invocation] db close forcedelete test.db test.db-journal test.db-wal sqlite3 db test.db diff --git a/test/shell4.test b/test/shell4.test index ee7d2b856f..068072202d 100644 --- a/test/shell4.test +++ b/test/shell4.test @@ -23,7 +23,8 @@ # set testdir [file dirname $argv0] source $testdir/tester.tcl -set CLI [test_find_cli] +set CLI [test_cli_invocation] +set CLI_ONLY [test_find_cli] db close forcedelete test.db test.db-journal test.db-wal sqlite3 db test.db @@ -130,13 +131,13 @@ do_test shell4-3.1 { set fd [open t1.txt wb] puts $fd "SELECT 'squirrel';" close $fd - exec $::CLI :memory: --interactive ".read t1.txt" + exec $::CLI_ONLY :memory: --interactive ".read t1.txt" } {squirrel} do_test shell4-3.2 { set fd [open t1.txt wb] puts $fd "SELECT 'pound: \302\243';" close $fd - exec $::CLI :memory: --interactive ".read t1.txt" + exec $::CLI_ONLY :memory: --interactive ".read t1.txt" } {pound: £} do_test shell4-4.1 { diff --git a/test/shell5.test b/test/shell5.test index 00d89f8d78..39018a0ce9 100644 --- a/test/shell5.test +++ b/test/shell5.test @@ -21,7 +21,7 @@ # set testdir [file dirname $argv0] source $testdir/tester.tcl -set CLI [test_find_cli] +set CLI [test_cli_invocation] db close forcedelete test.db test.db-journal test.db-wal diff --git a/test/shell8.test b/test/shell8.test index ddb4a47b82..bee6039232 100644 --- a/test/shell8.test +++ b/test/shell8.test @@ -19,7 +19,7 @@ set testprefix shell8 ifcapable !vtab { finish_test; return } -set CLI [test_find_cli] +set CLI [test_cli_invocation] # Check to make sure the shell has been compiled with ".archive" support. # diff --git a/test/swarmvtab3.test b/test/swarmvtab3.test index b062f9e952..8ca2471308 100644 --- a/test/swarmvtab3.test +++ b/test/swarmvtab3.test @@ -148,11 +148,13 @@ catch { array unset ::dbcache } # random integer between 0 and 1,000,000 # 0 and 99. do_test 2.1 { + catch { array unset ctx_used } for {set i 0} {$i < 100} {incr i} { while 1 { set ctx [expr abs(int(rand() *1000000))] - if {[info exists ::dbcache($ctx)]==0} break + if {[info exists ctx_used($ctx)]==0} break } + set ctx_used($ctx) 1 set file test_remote.db$ctx forcedelete $file diff --git a/test/tester.tcl b/test/tester.tcl index dd6512fe9a..e0ad25df1f 100644 --- a/test/tester.tcl +++ b/test/tester.tcl @@ -184,7 +184,7 @@ proc get_pwd {} { set comSpec {C:\Windows\system32\cmd.exe} } return [string map [list \\ /] \ - [string trim [exec -- $comSpec /c echo %CD%]]] + [string trim [exec -- $comSpec /c CD]]] } else { return [pwd] } @@ -2494,7 +2494,7 @@ proc test_find_binary {nm} { } # Find the name of the 'shell' executable (e.g. "sqlite3.exe") to use for -# the tests in shell[1-5].test. If no such executable can be found, invoke +# the tests in shell*.test. If no such executable can be found, invoke # [finish_test ; return] in the callers context. # proc test_find_cli {} { @@ -2503,6 +2503,37 @@ proc test_find_cli {} { return $prog } +# Find invocation of the 'shell' executable (e.g. "sqlite3.exe") to use +# for the tests in shell*.test with optional valgrind prefix when the +# environment variable SQLITE_CLI_VALGRIND_OPT is set. The set value +# operates as follows: +# empty or 0 => no valgrind prefix; +# 1 => valgrind options for memory leak check; +# other => use value as valgrind options. +# If shell not found, invoke [finish_test ; return] in callers context. +# +proc test_cli_invocation {} { + set prog [test_find_binary sqlite3] + if {$prog==""} { return -code return } + set vgrun [expr {[permutation]=="valgrind"}] + if {$vgrun || [info exists ::env(SQLITE_CLI_VALGRIND_OPT)]} { + if {$vgrun} { + set vgo "--quiet" + } else { + set vgo $::env(SQLITE_CLI_VALGRIND_OPT) + } + if {$vgo == 0 || $vgo eq ""} { + return $prog + } elseif {$vgo == 1} { + return "valgrind --quiet --leak-check=yes $prog" + } else { + return "valgrind $vgo $prog" + } + } else { + return $prog + } +} + # Find the name of the 'sqldiff' executable (e.g. "sqlite3.exe") to use for # the tests in sqldiff tests. If no such executable can be found, invoke # [finish_test ; return] in the callers context. diff --git a/test/upfrom4.test b/test/upfrom4.test new file mode 100644 index 0000000000..b20e9638aa --- /dev/null +++ b/test/upfrom4.test @@ -0,0 +1,130 @@ +# 2022-05-24 +# +# 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. +# +#*********************************************************************** +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix upfrom4 + +do_execsql_test 100 { + DROP TABLE IF EXISTS t5; + DROP TABLE IF EXISTS m1; + DROP TABLE IF EXISTS m2; + CREATE TABLE t5(a INTEGER PRIMARY KEY, b TEXT, c TEXT); + CREATE TABLE m1(x INTEGER PRIMARY KEY, y TEXT); + CREATE TABLE m2(u INTEGER PRIMARY KEY, v TEXT); + + INSERT INTO t5 VALUES(1, 'one', 'ONE'); + INSERT INTO t5 VALUES(2, 'two', 'TWO'); + INSERT INTO t5 VALUES(3, 'three', 'THREE'); + INSERT INTO t5 VALUES(4, 'four', 'FOUR'); + + INSERT INTO m1 VALUES(1, 'i'); + INSERT INTO m1 VALUES(2, 'ii'); + INSERT INTO m1 VALUES(3, 'iii'); + + INSERT INTO m2 VALUES(1, 'I'); + INSERT INTO m2 VALUES(3, 'II'); + INSERT INTO m2 VALUES(4, 'III'); + SELECT * FROM t5; +} {1 one ONE 2 two TWO 3 three THREE 4 four FOUR} + +do_execsql_test 110 { + BEGIN; + UPDATE t5 SET b=y, c=v FROM m1 LEFT JOIN m2 ON (x=u) WHERE x=a; + SELECT * FROM t5 ORDER BY a; + ROLLBACK; +} {1 i I 2 ii {} 3 iii II 4 four FOUR} + +do_execsql_test 120 { + BEGIN; + UPDATE t5 SET b=y, c=v FROM m2 RIGHT JOIN m1 ON (x=u) WHERE x=a; + SELECT * FROM t5 ORDER BY a; + ROLLBACK; +} {1 i I 2 ii {} 3 iii II 4 four FOUR} + + +reset_db +db null - +do_execsql_test 200 { + CREATE TABLE t1(a INT PRIMARY KEY, b INT, c INT); + INSERT INTO t1(a) VALUES(1),(2),(8),(19); + CREATE TABLE c1(x INTEGER PRIMARY KEY, b INT); + INSERT INTO c1(x,b) VALUES(1,1),(8,8),(17,17),(NULL,NULL); + CREATE TABLE c2(x INT,c INT); + INSERT INTO c2(x,c) VALUES(2,2),(8,8),(NULL,NULL); + CREATE TABLE dual(dummy TEXT); + INSERT INTO dual VALUES('X'); +} {} +do_execsql_test 210 { + BEGIN; + SELECT * FROM t1 ORDER BY a; + UPDATE t1 SET b=c1.b, c=c2.c + FROM dual, c1 NATURAL RIGHT JOIN c2 + WHERE x=a; + SELECT * FROM t1 ORDER BY a; + ROLLBACK; +} { + 1 - - + 2 - - + 8 - - + 19 - - + 1 - - + 2 - 2 + 8 8 8 + 19 - - +} +do_execsql_test 300 { + CREATE TABLE t2(x); + CREATE TRIGGER AFTER INSERT ON t2 BEGIN + UPDATE t1 SET b=c1.b, c=c2.c + FROM dual, c1 NATURAL RIGHT JOIN c2 + WHERE x=a; + END; +} {} +do_execsql_test 310 { + BEGIN; + SELECT * FROM t1 ORDER BY a; + INSERT INTO t2(x) VALUES(1); + SELECT * FROM t1 ORDER BY a; + ROLLBACK; +} { + 1 - - + 2 - - + 8 - - + 19 - - + 1 - - + 2 - 2 + 8 8 8 + 19 - - +} + +# 2022-05-26 dbsqlfuzz crash-9401d6ba699f1257d352a657de236286bf2b14da +# +reset_db +db null - +do_execsql_test 400 { + CREATE TABLE t2(x,y,z PRIMARY KEY) WITHOUT ROWID; + INSERT INTO t2 VALUES(89,-89,6); + CREATE TABLE t1(a INT,b TEXT,c TEXT,d REAL) STRICT; + INSERT INTO t1 VALUES(1,'xyz','def',4.5); + CREATE TRIGGER t1tr BEFORE UPDATE ON t1 BEGIN + INSERT INTO t1(a,b) VALUES(1000,'uvw'); + UPDATE t1 SET b=NULL FROM (SELECT CAST(a AS varchar) FROM t1 ORDER BY b) NATURAL LEFT FULL JOIN t1 AS text; + END; + UPDATE t1 SET b=b|100; + SELECT * FROM t1 ORDER BY a; +} { + 1 100 def 4.5 + 1000 - - - +} + +finish_test diff --git a/test/vtab6.test b/test/vtab6.test index 2ee5e27051..1c220e11fe 100644 --- a/test/vtab6.test +++ b/test/vtab6.test @@ -277,11 +277,15 @@ do_test vtab6-3.6 { SELECT * FROM t1 JOIN t2 ON t3.a=t2.b; } } {1 {no such column: t3.a}} + +# EVIDENCE-OF: R-47973-48020 you cannot say "INNER OUTER JOIN", because +# that would be contradictory. do_test vtab6-3.7 { catchsql { SELECT * FROM t1 INNER OUTER JOIN t2; } } {1 {unknown join type: INNER OUTER}} + do_test vtab6-3.7 { catchsql { SELECT * FROM t1 LEFT BOGUS JOIN t2; diff --git a/test/where.test b/test/where.test index 2f53f2cb49..e28861bc10 100644 --- a/test/where.test +++ b/test/where.test @@ -11,7 +11,6 @@ # This file implements regression tests for SQLite library. The # focus of this file is testing the use of indices in WHERE clases. # -# $Id: where.test,v 1.50 2008/11/03 09:06:06 danielk1977 Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl @@ -1603,4 +1602,19 @@ if {[permutation]!="valgrind"} { } {0} } +# 2022-05-10 dbsqlfuzz 4c5e3e89bc251d28378be88233f531b84ec66901 +# +reset_db +do_execsql_test where-28.1 { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b INT); + CREATE INDEX t1b ON t1(b,b,b,b,b,b,b,b,b,b,b,b,b); + INSERT INTO t1(a,b) VALUES(1,1),(15,2),(19,5); + UPDATE t1 SET b=999 WHERE a IN (SELECT 15) AND b IN (1,2); + SELECT * FROM t1; +} { + 1 1 + 15 999 + 19 5 +} + finish_test