]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add a demonstration sqlite3_vtab/module implemented in JS, based on ext/misc/template...
authorstephan <stephan@noemail.net>
Tue, 6 Dec 2022 06:09:03 +0000 (06:09 +0000)
committerstephan <stephan@noemail.net>
Tue, 6 Dec 2022 06:09:03 +0000 (06:09 +0000)
FossilOrigin-Name: 60482c97e02bc4cafefef281be0cf0bc8c5c53232162829c137f3f7a80cdc534

ext/wasm/GNUmakefile
ext/wasm/api/README.md
ext/wasm/api/sqlite3-api-oo1.js
ext/wasm/api/sqlite3-api-prologue.js
ext/wasm/api/sqlite3-v-helper.js [new file with mode: 0644]
ext/wasm/api/sqlite3-vfs-helper.js [deleted file]
ext/wasm/jaccwabyt/jaccwabyt.js
ext/wasm/jaccwabyt/jaccwabyt.md
ext/wasm/tester1.c-pp.js
manifest
manifest.uuid

index 968d4f440c9b7bcfe13f9f0242bbe5b3e7ae4dbb..1b184376821e2e56e1aac7dff1236796a0e367cb 100644 (file)
@@ -289,7 +289,7 @@ sqlite3-api.jses += $(dir.api)/sqlite3-api-glue.js
 sqlite3-api.jses += $(sqlite3-api-build-version.js)
 sqlite3-api.jses += $(dir.api)/sqlite3-api-oo1.js
 sqlite3-api.jses += $(dir.api)/sqlite3-api-worker1.js
-sqlite3-api.jses += $(dir.api)/sqlite3-vfs-helper.js
+sqlite3-api.jses += $(dir.api)/sqlite3-v-helper.js
 sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs.c-pp.js
 sqlite3-api.jses += $(dir.api)/sqlite3-api-cleanup.js
 
index f9955a8957f24c1e37cda6e468fd2caf9fe1f6d4..ce34b26b8a43440f70f68567563a1457b058fbf7 100644 (file)
@@ -78,11 +78,10 @@ browser client:
       a Promise-based interface into the Worker #1 API. This is
       a far user-friendlier way to interface with databases running
       in a Worker thread.
-- **`sqlite3-vfs-helper.js`**\  
-  This internal-use-only file installs `sqlite3.VfsHelper` for use by
-  `sqlite3-*.js` files which create `sqlite3_vfs` implementations.
-  `sqlite3.VfsHelper` gets removed from the the `sqlite3` object after
-  the library is finished initializing.
+- **`sqlite3-v-helper.js`**\  
+  Installs `sqlite3.VfsHelper` and `sqlite3.VtabHelper` for use by
+  downstream code which creates `sqlite3_vfs` and `sqlite3_module`
+  implementations.
 - **`sqlite3-vfs-opfs.c-pp.js`**\  
   is an sqlite3 VFS implementation which supports Google Chrome's
   Origin-Private FileSystem (OPFS) as a storage layer to provide
index b377efc2427d24759b7eaa646b575ff621029962..6253c659fc25bd7740b6005fd677f721f6664ef8 100644 (file)
@@ -473,6 +473,15 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
     return rc;
   };
 
+  /**
+     Internal impl of the DB.selectArrays() and
+     selectObjects() methods.
+  */
+  const __selectAll =
+        (db, sql, bind, rowMode)=>db.exec({
+          sql, bind, rowMode, returnValue: 'resultRows'
+        });
+
   /**
      Expects to be given a DB instance or an `sqlite3*` pointer (may
      be null) and an sqlite3 API result code. If the result code is
@@ -1098,6 +1107,26 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
       return __selectFirstRow(this, sql, bind, {});
     },
 
+    /**
+       Runs the given SQL and returns an array of all results, with
+       each row represented as an array, as per the 'array' `rowMode`
+       option to `exec()`. An empty result set resolves
+       to an empty array. The second argument, if any, is treated as
+       the 'bind' option to a call to exec().
+    */
+    selectArrays: function(sql,bind){
+      return __selectAll(this, sql, bind, 'array');
+    },
+
+    /**
+       Works identically to selectArrays() except that each value
+       in the returned array is an object, as per the 'object' `rowMode`
+       option to `exec()`.
+    */
+    selectObjects: function(sql,bind){
+      return __selectAll(this, sql, bind, 'object');
+    },
+
     /**
        Returns the number of currently-opened Stmt handles for this db
        handle, or 0 if this DB instance is closed.
index a51e957f8ff0ea1243b21b4053d915d12c9cc547..558fcda1e0801facf4b1378cbeec558a63414c86 100644 (file)
@@ -1642,7 +1642,6 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
              some initializers. Retain them when running in test mode
              so that we can add tests for them. */
           delete sqlite3.util;
-          delete sqlite3.VfsHelper;
           delete sqlite3.StructBinder;
         }
         return sqlite3;
diff --git a/ext/wasm/api/sqlite3-v-helper.js b/ext/wasm/api/sqlite3-v-helper.js
new file mode 100644 (file)
index 0000000..8427226
--- /dev/null
@@ -0,0 +1,392 @@
+/*
+** 2022-11-30
+**
+** 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 installs sqlite3.VfsHelper, and object which exists to
+   assist in the creation of JavaScript implementations of sqlite3_vfs,
+   along with its virtual table counterpart, sqlite3.VtabHelper.
+*/
+'use strict';
+self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+  const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss;
+  const vh = Object.create(null), vt = Object.create(null);
+
+  sqlite3.VfsHelper = vh;
+  sqlite3.VtabHelper = vt;
+
+  /**
+     Installs a StructBinder-bound function pointer member of the
+     given name and function in the given StructType target object.
+     It creates a WASM proxy for the given function and arranges for
+     that proxy to be cleaned up when tgt.dispose() is called.  Throws
+     on the slightest hint of error, e.g. tgt is-not-a StructType,
+     name does not map to a struct-bound member, etc.
+
+     Returns a proxy for this function which is bound to tgt and takes
+     2 args (name,func). That function returns the same thing,
+     permitting calls to be chained.
+
+     If called with only 1 arg, it has no side effects but returns a
+     func with the same signature as described above.
+
+     If tgt.ondispose is set before this is called then it _must_
+     be an array, to which this function will append entries.
+
+     ACHTUNG: because we cannot generically know how to transform JS
+     exceptions into result codes, the installed functions do no
+     automatic catching of exceptions. It is critical, to avoid 
+     undefined behavior in the C layer, that methods mapped via
+     this function do not throw. The exception, as it were, to that
+     rule is...
+
+     If applyArgcCheck is true then each method gets wrapped in a
+     proxy which asserts that it is passed the expected number of
+     arguments, throwing if the argument count does not match
+     expectations. That is only intended for dev-time usage for sanity
+     checking, and will leave the C environment in an undefined
+     state. For non-dev-time use, it is a given that the C API will
+     never call one of the generated function wrappers with the wrong
+     argument count.
+  */
+  vh.installMethod = vt.installMethod = function callee(
+    tgt, name, func, applyArgcCheck = callee.installMethodArgcCheck
+  ){
+    if(!(tgt instanceof sqlite3.StructBinder.StructType)){
+      toss("Usage error: target object is-not-a StructType.");
+    }
+    if(1===arguments.length){
+      return (n,f)=>callee(tgt, n, f, applyArgcCheck);
+    }
+    if(!callee.argcProxy){
+      callee.argcProxy = function(tgt, funcName, func,sig){
+        return function(...args){
+          if(func.length!==arguments.length){
+            toss("Argument mismatch for",
+                 tgt.structInfo.name+"::"+funcName
+                 +": Native signature is:",sig);
+          }
+          return func.apply(this, args);
+        }
+      };
+      /* An ondispose() callback for use with
+         sqlite3.StructBinder-created types. */
+      callee.removeFuncList = function(){
+        if(this.ondispose.__removeFuncList){
+          this.ondispose.__removeFuncList.forEach(
+            (v,ndx)=>{
+              if('number'===typeof v){
+                try{wasm.uninstallFunction(v)}
+                catch(e){/*ignore*/}
+              }
+              /* else it's a descriptive label for the next number in
+                 the list. */
+            }
+          );
+          delete this.ondispose.__removeFuncList;
+        }
+      };
+    }/*static init*/
+    const sigN = tgt.memberSignature(name);
+    if(sigN.length<2){
+      toss("Member",name," is not a function pointer. Signature =",sigN);
+    }
+    const memKey = tgt.memberKey(name);
+    const fProxy = applyArgcCheck
+    /** This middle-man proxy is only for use during development, to
+        confirm that we always pass the proper number of
+        arguments. We know that the C-level code will always use the
+        correct argument count. */
+          ? callee.argcProxy(tgt, memKey, func, sigN)
+          : func;
+    const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
+    tgt[memKey] = pFunc;
+    if(!tgt.ondispose) tgt.ondispose = [];
+    if(!tgt.ondispose.__removeFuncList){
+      tgt.ondispose.push('ondispose.__removeFuncList handler',
+                         callee.removeFuncList);
+      tgt.ondispose.__removeFuncList = [];
+    }
+    tgt.ondispose.__removeFuncList.push(memKey, pFunc);
+    return (n,f)=>callee(tgt, n, f, applyArgcCheck);
+  }/*installMethod*/;
+  vh.installMethod.installMethodArgcCheck = false;
+
+  /**
+     Installs methods into the given StructType-type instance. Each
+     entry in the given methods object must map to a known member of
+     the given StructType, else an exception will be triggered.  See
+     installMethod() for more details, including the semantics of the
+     3rd argument.
+
+     As an exception to the above, if any two or more methods in the
+     2nd argument are the exact same function, installMethod() is
+     _not_ called for the 2nd and subsequent instances, and instead
+     those instances get assigned the same method pointer which is
+     created for the first instance. This optimization is primarily to
+     accommodate special handling of sqlite3_module::xConnect and
+     xCreate methods.
+
+     On success, returns this object. Throws on error.
+  */
+  vh.installMethods = vt.installMethods = function(
+    structType, methods, applyArgcCheck = vh.installMethod.installMethodArgcCheck
+  ){
+    const seen = new Map /* map of <Function, memberName> */;
+    for(const k of Object.keys(methods)){
+      const m = methods[k];
+      const prior = seen.get(m);
+      if(prior){
+        const mkey = structType.memberKey(k);
+        structType[mkey] = structType[structType.memberKey(prior)];
+      }else{
+        vh.installMethod(structType, k, m, applyArgcCheck);
+        seen.set(m, k);
+      }
+    }
+    return this;
+  };
+
+  /**
+     Uses sqlite3_vfs_register() to register the
+     sqlite3.capi.sqlite3_vfs-type vfs, which must have already been
+     filled out properly. If the 2nd argument is truthy, the VFS is
+     registered as the default VFS, else it is not.
+
+     On success, returns this object. Throws on error.
+  */
+  vh.registerVfs = function(vfs, asDefault=false){
+    if(!(vfs instanceof sqlite3.capi.sqlite3_vfs)){
+      toss("Expecting a sqlite3_vfs-type argument.");
+    }
+    const rc = capi.sqlite3_vfs_register(vfs.pointer, asDefault ? 1 : 0);
+    if(rc){
+      toss("sqlite3_vfs_register(",vfs,") failed with rc",rc);
+    }
+    if(vfs.pointer !== capi.sqlite3_vfs_find(vfs.$zName)){
+      toss("BUG: sqlite3_vfs_find(vfs.$zName) failed for just-installed VFS",
+           vfs);
+    }
+    return this;
+  };
+
+  /**
+     A wrapper for installMethods() or registerVfs() to reduce
+     installation of a VFS and/or its I/O methods to a single
+     call.
+
+     Accepts an object which contains the properties "io" and/or
+     "vfs", each of which is itself an object with following properties:
+
+     - `struct`: an sqlite3.StructType-type struct. This must be a
+       populated (except for the methods) object of type
+       sqlite3_io_methods (for the "io" entry) or sqlite3_vfs (for the
+       "vfs" entry).
+
+     - `methods`: an object mapping sqlite3_io_methods method names
+       (e.g. 'xClose') to JS implementations of those methods. The JS
+       implementations must be call-compatible with their native
+       counterparts.
+
+     For each of those object, this function passes its (`struct`,
+     `methods`, (optional) `applyArgcCheck`) properties to
+     this.installMethods().
+
+     If the `vfs` entry is set then:
+
+     - Its `struct` property is passed to this.registerVfs(). The
+       `vfs` entry may optionally have an `asDefault` property, which
+       gets passed as the 2nd argument to registerVfs().
+
+     - If `struct.$zName` is falsy and the entry has a string-type
+       `name` property, `struct.$zName` is set to the C-string form of
+       that `name` value before registerVfs() is called.
+
+     On success returns this object. Throws on error.
+  */
+  vh.installVfs = function(opt){
+    let count = 0;
+    const propList = ['io','vfs'];
+    for(const key of propList){
+      const o = opt[key];
+      if(o){
+        ++count;
+        this.installMethods(o.struct, o.methods, !!o.applyArgcCheck);
+        if('vfs'===key){
+          if(!o.struct.$zName && 'string'===typeof o.name){
+            o.struct.$zName = wasm.allocCString(o.name);
+            /* Note that we leak that C-string. */
+          }
+          this.registerVfs(o.struct, !!o.asDefault);
+        }
+      }
+    }
+    if(!count) toss("Misuse: installVfs() options object requires at least",
+                    "one of:", propList);
+    return this;
+  };
+
+  /**
+     Expects to be passed the (argc,argv) arguments of
+     sqlite3_module::xFilter(), or an equivalent API.  This function
+     transforms the arguments (an array of (sqlite3_value*)) into a JS
+     array of equivalent JS values. It uses the same type conversions
+     as sqlite3_create_function_v2() and friends. Throws on error,
+     e.g. if it cannot figure out a sensible data conversion.
+  */
+  vt.sqlite3ValuesToJs = capi.sqlite3_create_function_v2.udfConvertArgs;
+
+  /**
+     Factory function for xyz2js() impls.
+  */
+  const __v2jsFactory = function(structType){
+    return function(ptr,remove=false){
+      if(0===arguments.length) ptr = new structType;
+      if(ptr instanceof structType){
+        //T.assert(!this.has(ptr.pointer));
+        this.set(ptr.pointer, ptr);
+        return ptr;
+      }else if(!wasm.isPtr(ptr)){
+        sqlite3.SQLite3Error.toss("Invalid argument to v2jsFactory");
+      }
+      let rc = this.get(ptr);
+      if(remove) this.delete(ptr);
+      /*arguable else if(!rc){
+        rc = new structType(ptr);
+        this.set(ptr, rc);
+      }*/
+      return rc;
+    }.bind(new Map);
+  };
+  /**
+     EXPERIMENTAL. DO NOT USE IN CLIENT CODE.
+
+     Has 3 distinct uses:
+
+     - vtab2js() instantiates a new capi.sqlite3_vtab instance, maps
+       its pointer for later by-pointer lookup, and returns that
+       object. This is intended to be called from
+       sqlite3_module::xConnect() or xCreate() implementations.
+
+     - vtab2js(pVtab) accepts a WASM pointer to a C-level
+       (sqlite3_vtab*) instance and returns the capi.sqlite3_vtab
+       object created by the first form of this function, or undefined
+       if that form has not been used. This is intended to be called
+       from sqlite3_module methods which take a (sqlite3_vtab*) pointer
+       _except_ for xDisconnect(), in which case use...
+
+     - vtab2js(pVtab,true) as for the previous form, but removes the
+       pointer-to-object mapping before returning.  The caller must
+       call dispose() on the returned object. This is intended to be
+       called from sqlite3_module::xDisconnect() implementations or
+       in error handling of a failed xCreate() or xConnect().
+ */
+  vt.vtab2js = __v2jsFactory(capi.sqlite3_vtab);
+
+  /**
+     EXPERIMENTAL. DO NOT USE IN CLIENT CODE.
+
+     Works identically to vtab2js() except that it deals with
+     sqlite3_cursor objects and pointers instead of sqlite3_vtab.
+
+     - vcur2js() is intended to be called from sqlite3_module::xOpen()
+
+     - vcur2js(pCursor) is intended to be called from all sqlite3_module
+       methods which take a (sqlite3_vtab_cursor*) _except_ for
+       xClose(), in which case use...
+
+     - vcur2js(pCursor, true) will remove the m apping of pCursor to a
+       capi.sqlite3_vtab_cursor object and return that object.  The
+       caller must call dispose() on the returned object. This is
+       intended to be called form xClose() or in error handling of a
+       failed xOpen().
+ */
+  vt.vcur2js = __v2jsFactory(capi.sqlite3_vtab_cursor);
+
+  /**
+     Given an error object, this function returns
+     sqlite3.capi.SQLITE_NOMEM if (e instanceof
+     sqlite3.WasmAllocError), else it returns its
+     second argument. Its intended usage is in the methods
+     of a sqlite3_vfs or sqlite3_module:
+
+     ```
+     try{
+      let rc = ...
+      return rc;
+     }catch(e){
+       return sqlite3.VtabHelper.exceptionToRc(e, sqlite3.capi.SQLITE_XYZ);
+       // where SQLITE_XYZ is some call-appropriate result code.
+     }
+     ```
+  */
+  /**vh.exceptionToRc = vt.exceptionToRc =
+    (e, defaultRc=capi.SQLITE_ERROR)=>(
+      (e instanceof sqlite3.WasmAllocError)
+        ? capi.SQLITE_NOMEM
+        : defaultRc
+    );*/
+
+  /**
+     Given an sqlite3_module method name and error object, this
+     function returns sqlite3.capi.SQLITE_NOMEM if (e instanceof
+     sqlite3.WasmAllocError), else it returns its second argument. Its
+     intended usage is in the methods of a sqlite3_vfs or
+     sqlite3_module:
+
+     ```
+     try{
+      let rc = ...
+      return rc;
+     }catch(e){
+       return sqlite3.VtabHelper.xMethodError(
+                'xColumn', e, sqlite3.capi.SQLITE_XYZ);
+       // where SQLITE_XYZ is some call-appropriate result code
+       // defaulting to SQLITE_ERROR.
+     }
+     ```
+
+     If xMethodError.errorReporter is a function, it is called in
+     order to report the error, else the error is not reported.
+     If that function throws, that exception is ignored.
+  */
+  vt.xMethodError = function f(methodName, err, defaultRc=capi.SQLITE_ERROR){
+    if(f.errorReporter instanceof Function){
+      try{f.errorReporter("sqlite3_module::"+methodName+"(): "+err.message);}
+      catch(e){/*ignored*/}
+    }
+    return (err instanceof sqlite3.WasmAllocError)
+      ? capi.SQLITE_NOMEM
+      : defaultRc;
+  };
+  vt.xMethodError.errorReporter = 1 ? console.error.bind(console) : false;
+
+  /**
+     "The problem" with this is that it introduces an outer function with
+     a different arity than the passed-in method callback. That means we
+     cannot do argc validation on these. Additionally, some methods (namely
+     xConnect) may have call-specific error handling. It would be a shame to
+     hard-coded that per-method support in this function.
+  */
+  /** vt.methodCatcher = function(methodName, method, defaultErrRc=capi.SQLITE_ERROR){
+    return function(...args){
+      try { method(...args); }
+      }catch(e){ return vt.xMethodError(methodName, e, defaultRc) }
+  };
+  */
+
+  /**
+     A helper for sqlite3_vtab::xRow() implementations. It must be
+     passed that function's 2nd argument and the value for that
+     pointer.  Returns the same as wasm.setMemValue() and will throw
+     if the 1st or 2nd arguments are invalid for that function.
+  */
+  vt.setRowId = (ppRowid64, value)=>wasm.setMemValue(ppRowid64, value, 'i64');
+}/*sqlite3ApiBootstrap.initializers.push()*/);
diff --git a/ext/wasm/api/sqlite3-vfs-helper.js b/ext/wasm/api/sqlite3-vfs-helper.js
deleted file mode 100644 (file)
index f9d3c18..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
-** 2022-11-30
-**
-** 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 installs sqlite.VfsHelper, an object which exists
-   to assist in the creation of JavaScript implementations of
-   sqlite3_vfs. It is NOT part of the public API, and is an
-   internal implemenation detail for use in this project's
-   own development of VFSes. It may be exposed to clients
-   at some point, provided there is value in doing so.
-*/
-'use strict';
-self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
-  const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss;
-  const vh = Object.create(null);
-
-  /**
-     Does nothing more than holds a permanent reference to each
-     argument. This is useful in some cases to ensure that, e.g., a
-     custom sqlite3_io_methods instance does not get
-     garbage-collected.
-
-     Returns this object.
-  */
-  vh.holdReference = function(...args){
-    for(const v of args) this.refs.add(v);
-    return vh;
-  }.bind({refs: new Set});
-  
-  /**
-     Installs a StructBinder-bound function pointer member of the
-     given name and function in the given StructType target object.
-     It creates a WASM proxy for the given function and arranges for
-     that proxy to be cleaned up when tgt.dispose() is called.  Throws
-     on the slightest hint of error, e.g. tgt is-not-a StructType,
-     name does not map to a struct-bound member, etc.
-
-     If applyArgcCheck is true then each method gets wrapped in a
-     proxy which asserts that it is passed the expected number of
-     arguments, throwing if the argument count does not match
-     expectations. That is only recommended for dev-time usage for
-     sanity checking. Once a VFS implementation is known to be
-     working, it is a given that the C API will never call it with the
-     wrong argument count.
-
-     Returns a proxy for this function which is bound to tgt and takes
-     2 args (name,func). That function returns the same thing,
-     permitting calls to be chained.
-
-     If called with only 1 arg, it has no side effects but returns a
-     func with the same signature as described above.
-
-     If tgt.ondispose is set before this is called then it _must_
-     be an array, to which this function will append entries.
-  */
-  vh.installMethod = function callee(tgt, name, func,
-                                     applyArgcCheck=callee.installMethodArgcCheck){
-    if(!(tgt instanceof sqlite3.StructBinder.StructType)){
-      toss("Usage error: target object is-not-a StructType.");
-    }
-    if(1===arguments.length){
-      return (n,f)=>callee(tgt, n, f, applyArgcCheck);
-    }
-    if(!callee.argcProxy){
-      callee.argcProxy = function(func,sig){
-        return function(...args){
-          if(func.length!==arguments.length){
-            toss("Argument mismatch. Native signature is:",sig);
-          }
-          return func.apply(this, args);
-        }
-      };
-      /* An ondispose() callback for use with
-         sqlite3.StructBinder-created types. */
-      callee.removeFuncList = function(){
-        if(this.ondispose.__removeFuncList){
-          this.ondispose.__removeFuncList.forEach(
-            (v,ndx)=>{
-              if('number'===typeof v){
-                try{wasm.uninstallFunction(v)}
-                catch(e){/*ignore*/}
-              }
-              /* else it's a descriptive label for the next number in
-                 the list. */
-            }
-          );
-          delete this.ondispose.__removeFuncList;
-        }
-      };
-    }/*static init*/
-    const sigN = tgt.memberSignature(name);
-    if(sigN.length<2){
-      toss("Member",name," is not a function pointer. Signature =",sigN);
-    }
-    const memKey = tgt.memberKey(name);
-    const fProxy = applyArgcCheck
-    /** This middle-man proxy is only for use during development, to
-        confirm that we always pass the proper number of
-        arguments. We know that the C-level code will always use the
-        correct argument count. */
-          ? callee.argcProxy(func, sigN)
-          : func;
-    const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
-    tgt[memKey] = pFunc;
-    if(!tgt.ondispose) tgt.ondispose = [];
-    if(!tgt.ondispose.__removeFuncList){
-      tgt.ondispose.push('ondispose.__removeFuncList handler',
-                         callee.removeFuncList);
-      tgt.ondispose.__removeFuncList = [];
-    }
-    tgt.ondispose.__removeFuncList.push(memKey, pFunc);
-    return (n,f)=>callee(tgt, n, f, applyArgcCheck);
-  }/*installMethod*/;
-  vh.installMethod.installMethodArgcCheck = false;
-
-  /**
-     Installs methods into the given StructType-type object. Each
-     entry in the given methods object must map to a known member of
-     the given StructType, else an exception will be triggered.
-     See installMethod() for more details, including the semantics
-     of the 3rd argument.
-
-     On success, passes its first argument to holdRefence() and
-     returns this object. Throws on error.
-  */
-  vh.installMethods = function(structType, methods,
-                               applyArgcCheck=vh.installMethod.installMethodArgcCheck){
-    for(const k of Object.keys(methods)){
-      vh.installMethod(structType, k, methods[k], applyArgcCheck);
-    }
-    return vh.holdReference(structType);
-  };
-
-  /**
-     Uses sqlite3_vfs_register() to register the
-     sqlite3.capi.sqlite3_vfs-type vfs, which must have already been
-     filled out properly. If the 2nd argument is truthy, the VFS is
-     registered as the default VFS, else it is not.
-
-     On success, passes its first argument to this.holdReference() and
-     returns this object. Throws on error.
-  */
-  vh.registerVfs = function(vfs, asDefault=false){
-    if(!(vfs instanceof sqlite3.capi.sqlite3_vfs)){
-      toss("Expecting a sqlite3_vfs-type argument.");
-    }
-    const rc = capi.sqlite3_vfs_register(vfs.pointer, asDefault ? 1 : 0);
-    if(rc){
-      toss("sqlite3_vfs_register(",vfs,") failed with rc",rc);
-    }
-    if(vfs.pointer !== capi.sqlite3_vfs_find(vfs.$zName)){
-      toss("BUG: sqlite3_vfs_find(vfs.$zName) failed for just-installed VFS",
-           vfs);
-    }
-    return vh.holdReference(vfs);
-  };
-
-  /**
-     A wrapper for installMethods() or registerVfs() to reduce
-     installation of a VFS and/or its I/O methods to a single
-     call.
-
-     Accepts an object which contains the properties "io" and/or
-     "vfs", each of which is itself an object with following properties:
-
-     - `struct`: an sqlite3.StructType-type struct. This must be a
-       populated (except for the methods) object of type
-       sqlite3_io_methods (for the "io" entry) or sqlite3_vfs (for the
-       "vfs" entry).
-
-     - `methods`: an object mapping sqlite3_io_methods method names
-       (e.g. 'xClose') to JS implementations of those methods.
-
-     For each of those object, this function passes its (`struct`,
-     `methods`, (optional) `applyArgcCheck`) properties to
-     this.installMethods().
-
-     If the `vfs` entry is set then:
-
-     - Its `struct` property is passed to this.registerVfs(). The
-       `vfs` entry may optionally have an `asDefault` property, which
-       gets passed as the 2nd argument to registerVfs().
-
-     - If `struct.$zName` is falsy and the entry has a string-type
-       `name` property, `struct.$zName` is set to the C-string form of
-       that `name` value before registerVfs() is called.
-
-     On success returns this object. Throws on error.
-  */
-  vh.installVfs = function(opt){
-    let count = 0;
-    const propList = ['io','vfs'];
-    for(const key of propList){
-      const o = opt[key];
-      if(o){
-        ++count;
-        this.installMethods(o.struct, o.methods, !!o.applyArgcCheck);
-        if('vfs'===key){
-          if(!o.struct.$zName && 'string'===typeof o.name){
-            o.struct.$zName = wasm.allocCString(o.name);
-            /* Note that we leak that C-string. */
-          }
-          this.registerVfs(o.struct, !!o.asDefault);
-        }
-      }
-    }
-    if(!count) toss("Misuse: installVfs() options object requires at least",
-                    "one of:", propList);
-    return this;
-  };
-  
-  sqlite3.VfsHelper = vh;
-}/*sqlite3ApiBootstrap.initializers.push()*/);
index 0bd7869bc6f8766d251488845f201d41cd9e52ac..1fa6baf154f5dd011127bee2006353019ff1a124 100644 (file)
@@ -231,6 +231,7 @@ self.Jaccwabyt = function StructBinderFactory(config){
         obj.ondispose.forEach(function(x){
           try{
             if(x instanceof Function) x.call(obj);
+            else if(x instanceof StructType) x.dispose();
             else if('number' === typeof x) dealloc(x);
             // else ignore. Strings are permitted to annotate entries
             // to assist in debugging.
index c6cf5c8a253afe6adccf238b62ae6112ac4594a9..17ada1edf0ee608b6ecc120492773b3c1e47d97e 100644 (file)
@@ -426,7 +426,8 @@ simply passing a pointer to the constructor. For example:
 
 ```js
 const m = new MyStruct( functionReturningASharedPtr() );
-// calling m.dispose() will _not_ free the wrapped C-side instance.
+// calling m.dispose() will _not_ free the wrapped C-side instance
+// but will trigger any ondispose handler.
 ```
 
 Now that we have struct instances, there are a number of things we
index 5db00cee64b7f5d538a829ee33648856027c87e0..6fcc8953299a3fe89784d703876e39d16fa85406 100644 (file)
@@ -419,6 +419,7 @@ self.sqlite3InitModule = sqlite3InitModule;
   ////////////////////////////////////////////////////////////////////
   T.g('C/WASM Utilities')
     .t('sqlite3.wasm namespace', function(sqlite3){
+      // TODO: break this into smaller individual test functions.
       const w = wasm;
       const chr = (x)=>x.charCodeAt(0);
       //log("heap getters...");
@@ -974,7 +975,7 @@ self.sqlite3InitModule = sqlite3InitModule;
     .t('Create db', function(sqlite3){
       const dbFile = '/tester1.db';
       wasm.sqlite3_wasm_vfs_unlink(0, dbFile);
-      const db = this.db = new sqlite3.oo1.DB(dbFile);
+      const db = this.db = new sqlite3.oo1.DB(dbFile, 0 ? 'ct' : 'c');
       T.assert(Number.isInteger(db.pointer))
         .mustThrowMatching(()=>db.pointer=1, /read-only/)
         .assert(0===sqlite3.capi.sqlite3_extended_result_codes(db.pointer,1))
@@ -1197,6 +1198,29 @@ self.sqlite3InitModule = sqlite3InitModule;
       rc = db.selectArray('select a, b from t where b=-1');
       T.assert(undefined === rc);
     })
+  ////////////////////////////////////////////////////////////////////////
+    .t('selectArrays/Objects()', function(sqlite3){
+      const db = this.db;
+      const sql = 'select a, b from t where a=? or b=? order by a';
+      let rc = db.selectArrays(sql, [1, 4]);
+      T.assert(Array.isArray(rc))
+        .assert(2===rc.length)
+        .assert(2===rc[0].length)
+        .assert(1===rc[0][0])
+        .assert(2===rc[0][1])
+        .assert(3===rc[1][0])
+        .assert(4===rc[1][1])
+      rc = db.selectArrays(sql, [99,99]);
+      T.assert(Array.isArray(rc)).assert(0===rc.length);
+      rc = db.selectObjects(sql, [1,4]);
+      T.assert(Array.isArray(rc))
+        .assert(2===rc.length)
+        .assert('object' === typeof rc[1])
+        .assert(1===rc[0].a)
+        .assert(2===rc[0].b)
+        .assert(3===rc[1].a)
+        .assert(4===rc[1].b);
+    })
 
   ////////////////////////////////////////////////////////////////////////
     .t({
@@ -1503,6 +1527,173 @@ self.sqlite3InitModule = sqlite3InitModule;
       T.mustThrow(()=>db.exec("select * from foo.bar"));
     })
 
+  ////////////////////////////////////////////////////////////////////////
+    .t({
+      name: 'Custom virtual tables',
+      predicate: ()=>wasm.bigIntEnabled,
+      test: function(sqlite3){
+        warn("The vtab/module JS bindings are experimental and subject to change.");
+        const vth = sqlite3.VtabHelper;
+        const tmplCols = Object.assign(Object.create(null),{
+          A: 0, B: 1
+        });
+        /**
+           The vtab demonstrated here is a JS-ification of
+           ext/misc/templatevtab.c.
+        */
+        const tmplMethods = {
+          xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){
+            try{
+              const rc = capi.sqlite3_declare_vtab(
+                pDb, "CREATE TABLE ignored(a,b)"
+              );
+              if(0===rc){
+                const t = vth.vtab2js();
+                wasm.setPtrValue(ppVtab, t.pointer);
+                T.assert(t === vth.vtab2js(wasm.getPtrValue(ppVtab)));
+              }
+              return rc;
+            }catch(e){
+              if(!(e instanceof sqlite3.WasmAllocError)){
+                wasm.setPtrValue(pzErr, wasm.allocCString(e.message));
+              }
+              return vth.xMethodError('xConnect',e);
+            }
+          },
+          xDisconnect: function(pVtab){
+            try {
+              const t = vth.vtab2js(pVtab, true);
+              t.dispose();
+              return 0;
+            }catch(e){
+              return vth.xMethodError('xDisconnect',e);
+            }
+          },
+          xOpen: function(pVtab, ppCursor){
+            try{
+              const t = vth.vtab2js(pVtab), c = vth.vcur2js();
+              T.assert(t instanceof capi.sqlite3_vtab);
+              T.assert(c instanceof capi.sqlite3_vtab_cursor);
+              wasm.setPtrValue(ppCursor, c.pointer);
+              c._rowId = 0;
+              return 0;
+            }catch(e){
+              return vth.xMethodError('xOpen',e);
+            }
+          },
+          xClose: function(pCursor){
+            try{
+              const c = vth.vcur2js(pCursor,true);
+              T.assert(c instanceof capi.sqlite3_vtab_cursor)
+                .assert(!vth.vcur2js(pCursor));
+              c.dispose();
+              return 0;
+            }catch(e){
+              return vth.xMethodError('xClose',e);
+            }
+          },
+          xNext: function(pCursor){
+            try{
+              const c = vth.vcur2js(pCursor);
+              ++c._rowId;
+              return 0;
+            }catch(e){
+              return vth.xMethodError('xNext',e);
+            }
+
+          },
+          xColumn: function(pCursor, pCtx, iCol){
+            try{
+              const c = vth.vcur2js(pCursor);
+              switch(iCol){
+                  case tmplCols.A:
+                    capi.sqlite3_result_int(pCtx, 1000 + c._rowId);
+                    break;
+                  case tmplCols.B:
+                    capi.sqlite3_result_int(pCtx, 2000 + c._rowId);
+                    break;
+                  default: sqlite3.SQLite3Error.toss("Invalid column id",iCol);
+              }
+              return 0;
+            }catch(e){
+              return vth.xMethodError('xColumn',e);
+            }
+          },
+          xRowid: function(pCursor, ppRowid64){
+            try{
+              const c = vth.vcur2js(pCursor);
+              vth.setRowId(ppRowid64, c._rowId);
+              return 0;
+            }catch(e){
+              return vth.xMethodError('xRowid',e);
+            }
+          },
+          xEof: function(pCursor){
+            const c = vth.vcur2js(pCursor);
+            return c._rowId>=10;
+          },
+          xFilter: function(pCursor, idxNum, idxCStr,
+                            argc, argv/* [sqlite3_value* ...] */){
+            try{
+              const c = vth.vcur2js(pCursor);
+              c._rowId = 0;
+              return 0;
+            }catch(e){
+              return vth.xMethodError('xFilter',e);
+            }
+          },
+          xBestIndex: function(pVtab, pIdxInfo){
+            try{
+              const t = vth.vtab2js(pVtab);
+              const pii = new capi.sqlite3_index_info(pIdxInfo);
+              pii.$estimatedRows = 10;
+              pii.$estimatedCost = 10.0;
+              pii.dispose();
+              return 0;
+            }catch(e){
+              return vth.xMethodError('xBestIndex',e);
+            }
+          }
+        };
+        /**
+           Problem to resolve: the vtab API places relevance on
+           whether xCreate and xConnect are exactly the same function
+           (same pointer address). Two JS-side references to the same
+           method will end up, without acrobatics to counter it, being
+           compiled as two different WASM-side bindings, i.e. two
+           different pointers.
+
+           In order to account for this, VtabHelper.installMethods()
+           checks for duplicate function entries and maps them to the
+           same WASM-compiled instance
+        */
+        if(1){
+          tmplMethods.xCreate = tmplMethods.xConnect;
+        }
+
+        const tmplMod = new sqlite3.capi.sqlite3_module();
+        tmplMod.ondispose = [];
+        tmplMod.$iVersion = 0;
+        vth.installMethods(tmplMod, tmplMethods, true);
+        if(tmplMethods.xCreate){
+          T.assert(tmplMod.$xCreate === tmplMod.$xConnect,
+                   "installMethods() must avoid re-compiling identical functions");
+          tmplMod.$xCreate = 0;
+        }
+        let rc = capi.sqlite3_create_module(
+          this.db, "testvtab", tmplMod, 0
+        );
+        this.db.checkRc(rc);
+
+        const list = this.db.selectArrays(
+          "SELECT a,b FROM testvtab order by a"
+        );
+        T.assert(10===list.length)
+          .assert(1000===list[0][0])
+          .assert(2009===list[list.length-1][1])
+      }
+    })/*vtab sanity checks*/
+
   ////////////////////////////////////////////////////////////////////
     .t({
       name: 'C-side WASM tests (if compiled in)',
@@ -1590,8 +1781,8 @@ self.sqlite3InitModule = sqlite3InitModule;
     }/* jaccwabyt-specific tests */)
 
     .t('Close db', function(){
-      T.assert(this.db).assert(Number.isInteger(this.db.pointer));
-      wasm.exports.sqlite3_wasm_db_reset(this.db.pointer);
+      T.assert(this.db).assert(wasm.isPtr(this.db.pointer));
+      wasm.sqlite3_wasm_db_reset(this.db);
       this.db.close();
       T.assert(!this.db.pointer);
     })
index a3f0d928c30b521b4875f2238b490dce7476d529..3a917c319639e0fe025e4cb43f81c9128593efd7 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Remove\stwo\sfeatures\sof\sjaccwabyt\swhich\swere\sfundamentally\sflawed,\salong\swith\sapprox.\s250\slines\sof\sunit\stests\swhich\sheavily\srelied\son\sthem.\sThankfully,\snone\sof\sthe\ssqlite3.js-level\scode\sused\sthose\sbits.
-D 2022-12-05T15:05:46.306
+C Add\sa\sdemonstration\ssqlite3_vtab/module\simplemented\sin\sJS,\sbased\son\sext/misc/templatevtab.c.\sAdd\soo1.selectArrays()\sand\sselectObjects().
+D 2022-12-06T06:09:03.466
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -491,12 +491,12 @@ F ext/userauth/sqlite3userauth.h 7f3ea8c4686db8e40b0a0e7a8e0b00fac13aa7a3
 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04
 F ext/userauth/userauth.c 7f00cded7dcaa5d47f54539b290a43d2e59f4b1eb5f447545fa865f002fc80cb
 F ext/wasm/EXPORTED_FUNCTIONS.fiddle.in 27450c8b8c70875a260aca55435ec927068b34cef801a96205adb81bdcefc65c
-F ext/wasm/GNUmakefile bfa47f169468ca9db031105b0e336db29a88e93c3abd217d0bbb2b8731fa5413
+F ext/wasm/GNUmakefile 54c0db93a5493f625c0a993c12aee5d83951440eee03b2aecfc8aeb998182998
 F ext/wasm/README-dist.txt 2d670b426fc7c613b90a7d2f2b05b433088fe65181abead970980f0a4a75ea20
 F ext/wasm/README.md ef39861aa21632fdbca0bdd469f78f0096f6449a720f3f39642594af503030e9
 F ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api ffa70413409e922ce0f761779787a1d9100b34b43c8e3106bb7ccf2786a41326
 F ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api 1ec3c73e7d66e95529c3c64ac3de2470b0e9e7fbf7a5b41261c367cf4f1b7287
-F ext/wasm/api/README.md 20a256f4aaae80035d2bb1c9e3e0a125570313a8d137d427471d7be10edde87a
+F ext/wasm/api/README.md 17fb1e10335cc87e366dec496c5b17b061f3f75cdf216e825258de34d97a3e53
 F ext/wasm/api/extern-post-js.c-pp.js 8923f76c3d2213159e12d641dc750523ead5c848185dc4996fae5cc12397f88d
 F ext/wasm/api/extern-pre-js.js cc61c09c7a24a07dbecb4c352453c3985170cec12b4e7e7e7a4d11d43c5c8f41
 F ext/wasm/api/post-js-footer.js cd0a8ec768501d9bd45d325ab0442037fb0e33d1f3b4f08902f15c34720ee4a1
@@ -504,12 +504,12 @@ F ext/wasm/api/post-js-header.js 47b6b281f39ad59fa6e8b658308cd98ea292c286a68407b
 F ext/wasm/api/pre-js.c-pp.js b88499dc303c21fc3f55f2c364a0f814f587b60a95784303881169f9e91c1d5f
 F ext/wasm/api/sqlite3-api-cleanup.js 680d5ccfff54459db136a49b2199d9f879c8405d9c99af1dda0cc5e7c29056f4
 F ext/wasm/api/sqlite3-api-glue.js c3a11e1d0e6fd381f68f9e76ad01f3616a6b809fbf9f5aa8e323955c128a6811
-F ext/wasm/api/sqlite3-api-oo1.js 793883953d4024e7b8c5ee1c7a6cb49c18ca53a1d235a203f93746f8907d32ba
-F ext/wasm/api/sqlite3-api-prologue.js 815fef5ee93e1bb11ebec5a1d6a1b8ae2e47cfeb66dc5f6e93380ccce045f194
+F ext/wasm/api/sqlite3-api-oo1.js b970787aaf0bdd3a3df59cf66aeb84d0decaaa0529aed7eaf45121087181245f
+F ext/wasm/api/sqlite3-api-prologue.js 31cffd8ce212fad8d316a08decd864b0f614c5fce3686153707dffe40f8279e8
 F ext/wasm/api/sqlite3-api-worker1.js e94ba98e44afccfa482874cd9acb325883ade50ed1f9f9526beb9de1711f182f
 F ext/wasm/api/sqlite3-license-version-header.js a661182fc93fc2cf212dfd0b987f8e138a3ac98f850b1112e29b5fbdaecc87c3
 F ext/wasm/api/sqlite3-opfs-async-proxy.js f79dd8d98ef3e0b55c10bb2bee7a3840fa967318e1f577c156aafc34664271d1
-F ext/wasm/api/sqlite3-vfs-helper.js 4ad4faf02e1524bf0296be8452c00b5708dce6faf649468d0377e26a0b299263
+F ext/wasm/api/sqlite3-v-helper.js 4451763a0cd85734f0afe18b48918cb3c88ca99cef399b7c5f12119281e7b6a8 w ext/wasm/api/sqlite3-vfs-helper.js
 F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js 29d6487a26b2fb6a471cde52c37ffee7c27ed6a91914b308c247e0706f454ffb
 F ext/wasm/api/sqlite3-wasi.h 25356084cfe0d40458a902afb465df8c21fc4152c1d0a59b563a3fba59a068f9
 F ext/wasm/api/sqlite3-wasm.c 723522a6c2a2463884a83fa1cc7ae5770deaaf0856a1058cc1023b2bfa1c898b
@@ -539,8 +539,8 @@ F ext/wasm/fiddle/fiddle.js 974b995119ac443685d7d94d3b3c58c6a36540e9eb3fed7069d5
 F ext/wasm/fiddle/index.html 5daf54e8f3d7777cbb1ca4f93affe28858dbfff25841cb4ab81d694efed28ec2
 F ext/wasm/index-dist.html c806b6005145b71d64240606e9c6e0bf56878ee8829c66fe7486cebf34b0e6b1
 F ext/wasm/index.html f151b7c7b5cfdc066567d556acd168e769efd4e982286dc5f849a5ee69ecd0ff
-F ext/wasm/jaccwabyt/jaccwabyt.js b7261221133cda8d363f16ddbac8e5b671fd51ce962fc34dc10e738a293b696d
-F ext/wasm/jaccwabyt/jaccwabyt.md 72742a3205f1477de68086e7e4a854ed5f7d08dfcd6db54cdbea4dba4866f159
+F ext/wasm/jaccwabyt/jaccwabyt.js f4fc93375e9c40ef60f56cbecca1b4dc8bf4f53fab2c3abc860ed34890c5d32d
+F ext/wasm/jaccwabyt/jaccwabyt.md 4bf62f7519857cdd674594aba7436cc4fae177eefbfaabc00740e16d9a828bee
 F ext/wasm/module-symbols.html 980680c8acfa3c8ae6a5aa223512d1b8e78040ced20f8ba2c382129bc73ec028
 F ext/wasm/scratchpad-wasmfs-main.html 20cf6f1a8f368e70d01e8c17200e3eaa90f1c8e1029186d836d14b83845fbe06
 F ext/wasm/scratchpad-wasmfs-main.js 4c140457f4d6da9d646a49addd91edb6e9ad1643c6c48e3258b5bce24725dc18
@@ -555,7 +555,7 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555
 F ext/wasm/test-opfs-vfs.js 44363db07b2a20e73b0eb1808de4400ca71b703af718d0fa6d962f15e73bf2ac
 F ext/wasm/tester1-worker.html d43f3c131d88f10d00aff3e328fed13c858d674ea2ff1ff90225506137f85aa9
 F ext/wasm/tester1.c-pp.html d34bef3d48e5cbc1c7c06882ad240fec49bf88f5f65696cc2c72c416933aa406
-F ext/wasm/tester1.c-pp.js 8ed17c0e1f271e536cb7ccd86d3992785fc8bf2f94f9c2e0088ca670601ee087
+F ext/wasm/tester1.c-pp.js d96a77dbf0d8af11e3f3adb013bee2bfb5bf9410e3f5eade528d70104451dd10
 F ext/wasm/tests/opfs/concurrency/index.html 86d8ac435074d1e7007b91105f4897f368c165e8cecb6a9aa3d81f5cf5dcbe70
 F ext/wasm/tests/opfs/concurrency/test.js a98016113eaf71e81ddbf71655aa29b0fed9a8b79a3cdd3620d1658eb1cc9a5d
 F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2
@@ -2065,8 +2065,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 a329a809b5da135a9c251e4d5f637d45d01d0248110ac05f2ad8f01d9df38c64
-R 3d4ed37777b015bac598ce3a6734f5aa
+P a190abc307847174f36421eaa3f47ef349c6f84a2bb35857fa64f64bbe722708
+R 5c37541d673cc882e16d46aec14f6ca8
 U stephan
-Z 002b2dd6406bdc41efd7e7f9f5d7918f
+Z d89d1c25adf51ede4f60b7c8a7ea4184
 # Remove this line to create a well-formed Fossil manifest.
index 14e600f31ef26e9eb37331424b8324abd4205416..17c068d5db9939fae11e0c3a7f74fe03f37e2ee7 100644 (file)
@@ -1 +1 @@
-a190abc307847174f36421eaa3f47ef349c6f84a2bb35857fa64f64bbe722708
\ No newline at end of file
+60482c97e02bc4cafefef281be0cf0bc8c5c53232162829c137f3f7a80cdc534
\ No newline at end of file