]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Introducing kvvfs v2 for the JS bindings. Summary: no longer hard-coded to session...
authorstephan <stephan@noemail.net>
Mon, 8 Dec 2025 13:06:27 +0000 (13:06 +0000)
committerstephan <stephan@noemail.net>
Mon, 8 Dec 2025 13:06:27 +0000 (13:06 +0000)
FossilOrigin-Name: ec866b04d088e53b09764d00f5403902785328fd8f3cbf80d3affa166477711d

1  2 
ext/wasm/tester1.c-pp.js
manifest
manifest.uuid

index 33bf733e9cc2885e6fb9fd9ed597263d0500f1d3,06cc7da01b83a4d2a74ec19a93f2642b861c8b64..50631bc6e581284ac8a2459f9c30cc72ce576c8f
@@@ -2914,17 -2966,523 +2966,523 @@@ globalThis.sqlite3InitModule = sqlite3I
          }finally{
            if( db ) db.close();
          }
+         //console.debug("sessionStorage",globalThis.sessionStorage);
        }
      }/*kvvfs sanity checks*/)
 -            error("Begin vacuum/page size test...");
+     .t({
+       name: 'transient kvvfs',
+       //predicate: ()=>false,
+       test: function(sqlite3){
+         const filename = '.' /* preinstalled instance */;
+         const JDb = sqlite3.oo1.JsStorageDb;
+         const DB = sqlite3.oo1.DB;
+         T.mustThrowMatching(()=>new JDb(""), capi.SQLITE_MISUSE);
+         T.mustThrowMatching(()=>{
+           new JDb("this\ns an illegal - contains control characters");
+           /* We don't have a way to get error strings from xOpen()
+              to this point? xOpen() does not have a handle to the
+              db and SQLite is not calling xGetLastError() to fetch
+              the error string. */
+         }, capi.SQLITE_RANGE);
+         T.mustThrowMatching(()=>{new JDb("foo-journal");},
+                             capi.SQLITE_MISUSE);
+         T.mustThrowMatching(()=>{new JDb("foo-wal");},
+                             capi.SQLITE_MISUSE);
+         T.mustThrowMatching(()=>{new JDb("foo-shm");},
+                             capi.SQLITE_MISUSE);
+         T.mustThrowMatching(()=>{
+           new JDb("01234567890123456789"+
+                   "01234567890123456789"+
+                   "01234567890123456789"+
+                   "01234567890123456789"+
+                   "01234567890123456789"+
+                   "01234567890123456789"+
+                   "0"/*too long*/);
+         }, capi.SQLITE_RANGE);
+         {
+           const name = "01234567890123456789012" /* max name length */;
+           (new JDb(name)).close();
+           T.assert( sqlite3.kvvfs.unlink(name) );
+         }
+         sqlite3.kvvfs.clear(filename);
+         let db = new JDb(filename);
+         const sqlSetup = [
+           'create table kvvfs(a);',
+           'insert into kvvfs(a) values(1),(2),(3)'
+         ];
+         try {
+           T.assert( 0===db.storageSize(), "expecting 0 storage size" );
+           T.mustThrowMatching(()=>db.clearStorage(), /in-use/);
+           //db.clearStorage();
+           T.assert( 0===db.storageSize(), "expecting 0 storage size" );
+           db.exec(sqlSetup);
+           T.assert( 0<db.storageSize(), "expecting non-0 db size" );
+           T.mustThrowMatching(()=>db.clearStorage(), /in-use/);
+           //db.clearStorage(/*wiping everything out from under it*/);
+           T.assert( 0<db.storageSize(), "expecting non-0 storage size" );
+           //db.exec(sqlSetup/*that actually worked*/);
+           /* Clearing the storage out from under the db does actually
+              work so long as we re-initialize it before reading.
+              It is tentatively disallowed for sanity's sake rather
+              than it not working.
+            */
+           T.assert( 0<db.storageSize(), "expecting non-0 db size" );
+           const close = ()=>{
+             db.close();
+             db = undefined;
+           };
+           T.assert(3 === db.selectValue('select count(*) from kvvfs'));
+           close();
+           const exportDb = sqlite3.kvvfs.export;
+           db = new JDb(filename);
+           db.exec('insert into kvvfs(a) values(4),(5),(6)');
+           T.assert(6 === db.selectValue('select count(*) from kvvfs'));
+           const exp = exportDb({name:filename,includeJournal:true});
+           T.assert( filename===exp.name, "Broken export filename" )
+             .assert( exp?.size > 0, "Missing db size" )
+             .assert( exp?.pages?.length > 0, "Missing db pages" );
+           console.debug("kvvfs to Object:",exp);
+           close();
+           const dbFileRaw = 'file:new-storage?vfs=kvvfs&delete-on-close=1';
+           db = new DB({
+             filename: dbFileRaw,
+             //flags: 'ct'
+           });
+           db.exec(sqlSetup);
+           const dbFilename = db.dbFilename();
+           //console.warn("db.dbFilename() =",dbFilename);
+           T.assert(3 === db.selectValue('select count(*) from kvvfs'));
+           debug("kvvfs to Object:",exportDb(dbFilename));
+           const n = sqlite3.kvvfs.estimateSize( dbFilename );
+           T.assert( n>0, "Db size count failed" );
+           if( 1 ){
+             // Concurrent open of that same name uses the same storage
+             const x = new JDb(dbFilename);
+             T.assert(3 === db.selectValue('select count(*) from kvvfs'));
+             x.close();
+           }
+           close();
+           // When the final instance of a name is closed, the storage
+           // disappears...
+           T.mustThrowMatching(function(){
+             /* Ensure that 'new-storage' was deleted when its refcount
+                went to 0, because of its 'transient' flag. By default
+                the objects are retained, like a filesystem would. */
+             let ddb = new JDb(dbFilename);
+             try{ddb.selectValue('select a from kvvfs')}
+             finally{ddb.close()}
+           }, /no such table: kvvfs/);
+         }finally{
+           if( db ) db.close();
+         }
+       }
+     }/*transient kvvfs*/)
+     .t({
+       name: 'concurrent transient kvvfs',
+       //predicate: ()=>false,
+       test: function(sqlite3){
+         const filename = 'myStorage';
+         const kvvfs = sqlite3.kvvfs;
+         const DB = sqlite3.oo1.DB;
+         const JDb = sqlite3.oo1.JsStorageDb;
+         let db;
+         let duo;
+         let q1, q2;
+         const sqlSetup = [
+           'create table kvvfs(a);',
+           'insert into kvvfs(a) values(1),(2),(3)'
+         ];
+         const sqlCount = 'select count(*) from kvvfs';
+         try {
+           const exportDb = sqlite3.kvvfs.export;
+           const dbFileRaw = 'file:'+filename+'?vfs=kvvfs&delete-on-close=1';
+           sqlite3.kvvfs.clear(filename);
+           db = new DB(dbFileRaw);
+           db.exec(sqlSetup);
+           T.assert(3 === db.selectValue(sqlCount));
+           duo = new JDb(filename);
+           duo.exec('insert into kvvfs(a) values(4),(5),(6)');
+           T.assert(6 === db.selectValue(sqlCount));
+           const expOpt = {
+             name: filename,
+             decodePages: true
+           };
+           let exp = exportDb(expOpt);
+           let expectRows = 6;
+           debug("exported db",exp);
+           db.close();
+           T.assert(expectRows === duo.selectValue(sqlCount));
+           duo.close();
+           T.mustThrowMatching(function(){
+             let ddb = new JDb(filename);
+             try{ddb.selectValue('select a from kvvfs')}
+             finally{ddb.close()}
+           }, /.*no such table: kvvfs.*/);
+           T.assert( kvvfs.unlink(filename) )
+             .assert( !kvvfs.exists(filename) );
+           const importDb = sqlite3.kvvfs.import;
+           duo = new JDb(dbFileRaw);
+           T.mustThrowMatching(()=>importDb(exp,true), /.*in use.*/);
+           duo.close();
+           importDb(exp, true);
+           duo = new JDb(dbFileRaw);
+           T.assert(expectRows === duo.selectValue(sqlCount));
+           let newCount;
+           try{
+             duo.transaction(()=>{
+               duo.exec("insert into kvvfs(a) values(7)");
+               newCount = duo.selectValue(sqlCount);
+               T.assert(false, "rolling back");
+             });
+           }catch(e){/*ignored*/}
+           T.assert(7===newCount, "Unexpected row count before rollback")
+             .assert(expectRows === duo.selectValue(sqlCount),
+                     "Unexpected row count after rollback");
+           duo.close();
+           T.assert( kvvfs.unlink(filename) )
+             .assert( !kvvfs.exists(filename) );
+           importDb(exp, true);
+           db = new JDb({
+             filename,
+             flags: 'c'
+             /* BUG: without the 'c' flag, the db works until we try to
+                vacuum, at which point it fails with "read only db". */
+           });
+           duo = new JDb(filename);
+           T.assert(expectRows === duo.selectValue(sqlCount));
+           const sqlIns1 = "insert into kvvfs(a) values(?)";
+           q1 = db.prepare(sqlIns1);
+           q2 = duo.prepare(sqlIns1);
+           if( 0 ){
+             q1.bind('from q1').stepFinalize();
+             ++expectRows;
+             T.assert(expectRows === duo.selectValue(sqlCount),
+                      "Unexpected record count.");
+             q2.bind('from q1').stepFinalize();
+             ++expectRows;
+           }else{
+             q1.bind('from q1');
+             T.assert(capi.SQLITE_DONE===capi.sqlite3_step(q1),
+                      "Unexpected step result");
+             ++expectRows;
+             T.assert(expectRows === duo.selectValue(sqlCount),
+                      "Unexpected record count.");
+             q2.bind('from q1').step();
+             ++expectRows;
+           }
+           T.assert(expectRows === db.selectValue(sqlCount),
+                    "Unexpected record count.");
+           q1.finalize();
+           q2.finalize();
+           if( 1 ){
 -            error("End vacuum/page size test.");
++            debug("Begin vacuum/page size test...");
+             const defaultPageSize = 1024 * 8 /* build-time default */;
+             const pageSize = 0
+                   ? defaultPageSize
+                   : 1024 * 16 /* any valid value other than the default */;
+             if( 0 ){
+               debug("Export before vacuum", exportDb(expOpt));
+               debug("page size before vacuum",
+                     db.selectArray(
+                       "select page_size from pragma_page_size()"
+                     ));
+             }
+             //kvvfs.log.xFileControl = true;
+             //kvvfs.log.xAccess = true;
+             db.exec([
+               "BEGIN;",
+               "insert into kvvfs(a) values(randomblob(16000/*>pg size*/));",
+               "COMMIT;",
+               "delete from kvvfs where octet_length(a)>100;",
+               "pragma page_size="+pageSize+";",
+               "vacuum;",
+               "select 1;"
+             ]);
+             const expectPageSize = kvvfs.internal.disablePageSizeChange
+                   ? defaultPageSize
+                   : pageSize;
+             const gotPageSize = db.selectValue(
+               "select page_size from pragma_page_size()"
+             );
+             T.assert(+gotPageSize === expectPageSize,
+                      "Expecting page size",expectPageSize,
+                      "got",gotPageSize);
+             T.assert(expectRows === duo.selectValue(sqlCount),
+                      "Unexpected record count.");
+             kvvfs.log.xAccess = kvvfs.log.xFileControl = false;
+             T.assert(expectRows === duo.selectValue(sqlCount),
+                      "Unexpected record count.");
+             exp = exportDb(expOpt);
+             debug("Exported page-expanded db",exp);
+             if( 0 ){
+               debug("vacuumed export",exp);
+             }
++            debug("End vacuum/page size test.");
+           }else{
+             expectRows = 6;
+           }
+           db.close();
+           duo.close();
+           T.assert( kvvfs.unlink(exp.name) )
+             .assert( !kvvfs.exists(exp.name) );
+           importDb(exp);
+           T.assert( kvvfs.exists(exp.name) );
+           db = new JDb(exp.name);
+           //debug("column count after export",db.selectValue(sqlCount));
+           T.assert(expectRows === db.selectValue(sqlCount),
+                    "Unexpected record count.");
+           /*
+             TODO: more advanced concurrent use tests, e.g. looping
+             over a query in one connection while writing from
+             another. Currently that will probably corrupt the db, and
+             kvvfs's journaling does not support multiple journals per
+             storage unit. We need to test the locking and fix it as
+             appropriate.
+           */
+         }finally{
+           q1?.finalize?.();
+           q2?.finalize?.();
+           db?.close?.();
+           duo?.close?.();
+         }
+       }
+     }/*concurrent transient kvvfs*/)
+     .t({
+       name: 'kvvfs listeners (experiment)',
+       test: function(sqlite3){
+         const kvvfs = sqlite3.kvvfs;
+         const filename = 'listen';
+         let db;
+         try {
+           const DB = sqlite3.oo1.DB;
+           const sqlSetup = [
+             'create table kvvfs(a);',
+             'insert into kvvfs(a) values(1),(2),(3)'
+           ];
+           const sqlCount = "select count(*) from kvvfs";
+           const sqlSelectSchema = "select * from sqlite_schema";
+           const counts = Object.create(null);
+           const incr = (key)=>counts[key] = 1 + (counts[key] ?? 0);
+           const pglog = Object.assign(Object.create(null),{
+             pages: [],
+             jrnl: undefined,
+             size: undefined,
+             includeJournal: false,
+             decodePages: true,
+             exception: new Error("Testing that exceptions from listeners do not interfere") 
+          });
+           const toss = ()=>{
+             if( pglog.exception ){
+               const e = pglog.exception;
+               delete pglog.exception;
+               throw e;
+             }
+           };
+           const listener = {
+             storage: filename,
+             reserve: true,
+             includeJournal: pglog.includeJournal,
+             decodePages: pglog.decodePages,
+             events: {
+               /**
+                  These may be async but must not be in this case
+                  because we can't test their result without a lot of
+                  hoop-jumping if they are. Kvvfs calls these
+                  asynchronously, though.
+                */
+               'open':   (ev)=>{
+                 //console.warn('open',ev);
+                 incr(ev.type);
+                 T.assert(filename===ev.storageName)
+                   .assert('number'===typeof ev.data);
+               },
+               'close': (ev)=>{
+                 //console.warn('close',ev);
+                 incr(ev.type);
+                 T.assert('number'===typeof ev.data);
+                 toss();
+               },
+               'delete': (ev)=>{
+                 //console.warn('delete',ev);
+                 incr(ev.type);
+                 T.assert('string'===typeof ev.data);
+                 switch(ev.data){
+                   case 'jrnl':
+                     T.assert(pglog.includeJournal);
+                     pglog.jrnl = null;
+                     break;
+                   default:{
+                     const n = +ev.data;
+                     T.assert( n>0, "Expecting positive db page number" );
+                     if( n < pglog.pages.length ){
+                       pglog.size = undefined;
+                     }
+                     pglog.pages[n] = undefined;
+                     break;
+                   }
+                 }
+               },
+               'sync': (ev)=>{
+                 incr(ev.data ? 'xSync' : 'xFileControlSync');
+               },
+               'write':  (ev)=>{
+                 //console.warn('write',ev);
+                 incr(ev.type);
+                 T.assert(Array.isArray(ev.data));
+                 const key = ev.data[0], val = ev.data[1];
+                 T.assert('string'===typeof key);
+                 switch( key ){
+                   case 'jrnl':
+                     T.assert(pglog.includeJournal);
+                     pglog.jrnl = val;
+                     break;
+                   case 'sz':{
+                     const sz = +val;
+                     T.assert( sz>0, "Expecting a db page number" );
+                     if( sz < pglog.sz ){
+                       pglog.pages.length = sz / pglog.pages.length;
+                     }
+                     pglog.size = sz;
+                     break;
+                   }
+                   default:
+                     T.assert( +key>0, "Expecting a positive db page number" );
+                     pglog.pages[+key] = val;
+                     if( pglog.decodePages ){
+                       T.assert( val instanceof Uint8Array );
+                     }else{
+                       T.assert( 'string'===typeof val );
+                     }
+                     break;
+                 }
+               }
+             }
+           };
+           kvvfs.listen(listener);
+           const dbFileRaw = 'file:'+filename+'?vfs=kvvfs&delete-on-close=1';
+           const expOpt = {
+             name: filename,
+             //decodePages: true
+           };
+           db = new DB(dbFileRaw);
+           db.exec(sqlSetup);
+           T.assert(db.selectObjects(sqlSelectSchema)?.length>0,
+                    "Unexpected empty schema");
+           db.close();
+           debug("kvvfs listener counts:",counts);
+           T.assert( counts.open );
+           T.assert( counts.close );
+           T.assert( listener.includeJournal ? counts.delete : !counts.delete );
+           T.assert( counts.write );
+           T.assert( counts.xSync );
+           T.assert( counts.xFileControlSync>=counts.xSync );
+           T.assert( counts.open===counts.close );
+           T.assert( pglog.includeJournal
+                     ? (null===pglog.jrnl)
+                     : (undefined===pglog.jrnl),
+                      "Unexpected pglog.jrnl value: "+pglog.jrnl );
+           if( 1 ){
+             T.assert(undefined===pglog.pages[0], "Expecting empty slot 0");
+             pglog.pages.shift();
+             //debug("kvvfs listener pageLog", pglog);
+           }
+           const before = JSON.stringify(counts);
+           T.assert( kvvfs.unlisten(listener) );
+           T.assert( !kvvfs.unlisten(listener) );
+           db = new DB(dbFileRaw);
+           T.assert( db.selectObjects(sqlSelectSchema)?.length>0 );
+           const exp = kvvfs.export(expOpt);
+           const expectRows = db.selectValue(sqlCount);
+           db.exec("delete from kvvfs");
+           db.close();
+           const after = JSON.stringify(counts);
+           T.assert( before===after, "Expecting no events after unlistening." );
+           if( 0 ){
+             exp = kvvfs.export(expOpt);
+             debug("Post-delete export:",exp);
+           }
+           if( 1 ){
+             // Replace the storage with the pglog state...
+             const bogoExp = {
+               name: filename,
+               size: pglog.size,
+               timestamp: Date.now(),
+               pages: pglog.pages
+             };
+             //debug("exp",exp);
+             //debug("bogoExp",bogoExp);
+             kvvfs.import(bogoExp, true);
+             //debug("Re-exported", kvvfs.export(expOpt));
+             db = new DB(dbFileRaw);
+             // Failing on the next line despite exports looking good
+             T.assert(db.selectObjects(sqlSelectSchema)?.length>0,
+                      "Empty schema on imported db");
+             T.assert(expectRows===db.selectValue(sqlCount));
+             db.close();
+           }
+         }finally{
+           db?.close?.();
+           kvvfs.unlink(filename);
+         }
+       }
+     })/*kvvfs listeners */
+     .t({
+       name: 'kvvfs vtab',
+       predicate: (sqlite3)=>!!sqlite3.kvvfs.create_module,
+       test: function(sqlite3){
+         const kvvfs = sqlite3.kvvfs;
+         const db = new sqlite3.oo1.DB();
+         const db2 = new sqlite3.oo1.DB('file:foo?vfs=kvvfs&delete-on-close=1');
+         try{
+           kvvfs.create_module(db);
+           let rc = db.selectObjects("select * from sqlite_kvvfs order by name");
+           debug("sqlite_kvvfs vtab:", rc);
+           const nDb = rc.length;
+           rc = db.selectObject("select * from sqlite_kvvfs where name='foo'");
+           T.assert(rc, "Expecting foo storage record")
+             .assert('foo'===rc.name, "Unexpected name")
+             .assert(1===rc.nRef, "Unexpected refcount");
+           db2.close();
+           rc = db.selectObjects("select * from sqlite_kvvfs");
+           T.assert( !rc.filter((o)=>o.name==='foo').length,
+                     "Expecting foo storage to be gone");
+           debug("sqlite_kvvfs vtab:", rc);
+           T.assert( rc.length+1 === nDb,
+                     "Unexpected storage count: got",rc.length,"expected",nDb);
+         }finally{
+           db.close();
+           db2.close();
+         }
+       }
+     })/* kvvfs vtab */
  //#if enable-see
      .t({
-       name: 'kvvfs with SEE encryption',
-       predicate: ()=>(isUIThread()
-                       || "Only available in main thread."),
+       name: 'kvvfs SEE encryption in sessionStorage',
+       predicate: ()=>(!!globalThis.sessionStorage
+                       || "sessionStorage is not available"),
        test: function(sqlite3){
-         T.seeBaseCheck(sqlite3.oo1.JsStorageDb, (isInit)=>{
-           return {filename: "session"};
-         }, ()=>this.kvvfsUnlink());
+         const JDb = sqlite3.oo1.JsStorageDb;
+         T.seeBaseCheck(JDb,
+                        (isInit)=>return {filename: "session"},
+                        ()=>JDb.clearStorage('session'));
        }
      })/*kvvfs with SEE*/
  //#endif enable-see
diff --cc manifest
index 5be241e282ddce81a3b94858e041839378ac14e9,cb1384b5686990e3c924d1a7308e456d92ece9d9..a5b9b991685c49684c0894bfe6c6dc53a20d5695
+++ b/manifest
@@@ -1,5 -1,5 +1,5 @@@
- C Minor\stweaks\sto\sthe\sQRF\sdocumentation.\s\sNo\schanges\sto\scode.
- D 2025-12-07T18:19:22.124
 -C Replace\ssome\sdouble-quotes\swith\ssingle\squotes\san\sSQL\sdoc\ssnippet\sin\sthe\scsv\sexample\svirtual\stable.
 -D 2025-12-08T12:24:23.024
++C Introducing\skvvfs\sv2\sfor\sthe\sJS\sbindings.\sSummary:\sno\slonger\shard-coded\sto\ssession/localStorage,\savailable\soutside\sof\sthe\smain\sUI\sthread\s(non-persistently),\sa\ssimpler-to-use\simport/export\sAPI,\sand\sadds\san\sasynchronous\sevent\sinterface\sintended\sfor\screating\spage-by-page\sdb\sbackups.
++D 2025-12-08T13:06:27.801
  F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
  F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
  F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@@ -648,7 -647,7 +648,7 @@@ F ext/wasm/test-opfs-vfs.html 1f2d672f3
  F ext/wasm/test-opfs-vfs.js 1618670e466f424aa289859fe0ec8ded223e42e9e69b5c851f809baaaca1a00c
  F ext/wasm/tester1-worker.c-pp.html 0e432ec2c0d99cd470484337066e8d27e7aee4641d97115338f7d962bf7b081a
  F ext/wasm/tester1.c-pp.html 52d88fe2c6f21a046030a36410b4839b632f4424028197a45a3d5669ea724ddb
- F ext/wasm/tester1.c-pp.js 015b4133cc3a5fb41d6236a6b39d23d996cc2d61a4877acde31a1f69574d4ce3
 -F ext/wasm/tester1.c-pp.js e4714f2e9dfd8b84b75eed22118defa978cb0f2600b99bf0dd730852f4bcb42b
++F ext/wasm/tester1.c-pp.js 7bc90f6e3d133c735fad05d5409915bd1389f4b5d6ce7c5daca33856669e706b
  F ext/wasm/tests/opfs/concurrency/index.html 657578a6e9ce1e9b8be951549ed93a6a471f4520a99e5b545928668f4285fb5e
  F ext/wasm/tests/opfs/concurrency/test.js d08889a5bb6e61937d0b8cbb78c9efbefbf65ad09f510589c779b7cc6a803a88
  F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2
@@@ -719,14 -718,14 +719,14 @@@ F src/notify.c 57c2d1a2805d6dee32acd5d2
  F src/os.c 509452169d5ea739723e213b8e2481cf0e587f0e88579a912d200db5269f5f6d
  F src/os.h 1ff5ae51d339d0e30d8a9d814f4b8f8e448169304d83a7ed9db66a65732f3e63
  F src/os_common.h 6c0eb8dd40ef3e12fe585a13e709710267a258e2c8dd1c40b1948a1d14582e06
- F src/os_kv.c fb7ba8d6204197357f1eb7e1c7450d09c10043bf7e99aba602f4aa46b8fb11a3
+ F src/os_kv.c e7d96727db5b67e39d590a68cc61c86daf4c093c36c011a09ebfb521182ec28d
  F src/os_setup.h 8efc64eda6a6c2f221387eefc2e7e45fd5a3d5c8337a7a83519ba4fbd2957ae2
 -F src/os_unix.c 7945ede1e85b2d1b910e1b4af9ba342e964b1e30e79f4176480a60736445cb36
 -F src/os_win.c a89b501fc195085c7d6c9eec7f5bd782625e94bb2a96b000f4d009703df1083f
 -F src/os_win.h 4c247cdb6d407c75186c94a1e84d5a22cbae4adcec93fcae8d2bc1f956fd1f19
 +F src/os_unix.c dcf7988ddbdd68619b821c9a722f9377abb46f1d26c9279eb5a50402fd43d749
 +F src/os_win.c 7eb8a49b18ac8bbd0b8e31bf346469074b65d4cebd6ff7259d07190d0853b534
 +F src/os_win.h 5e168adf482484327195d10f9c3bce3520f598e04e07ffe62c9c5a8067c1037b
  F src/pager.c a81461de271ac4886ad75b7ca2cca8157a48635820c4646cd2714acdc2c17e5f
  F src/pager.h 6137149346e6c8a3ddc1eeb40aee46381e9bc8b0fcc6dda8a1efde993c2275b8
 -F src/parse.y 424d195ea70f4656a3f6440e0b79ca8f712ae4da9431033a19ec8c9816469287
 +F src/parse.y 7c2184b5665c671258c4e96a10bbc9dbf7e1ede462ebc4e614249de0d54c8a26
  F src/pcache.c 588cc3c5ccaaadde689ed35ce5c5c891a1f7b1f4d1f56f6cf0143b74d8ee6484
  F src/pcache.h 1497ce1b823cf00094bb0cf3bac37b345937e6f910890c626b16512316d3abf5
  F src/pcache1.c 131ca0daf4e66b4608d2945ae76d6ed90de3f60539afbd5ef9ec65667a5f2fcd
@@@ -2184,8 -2180,8 +2184,9 @@@ F tool/version-info.c 33d0390ef484b3b1c
  F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7
  F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98
  F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
- P 9766b47beb9ec72f55bfe9fa6e4dadf1829848389388aa30e97094a325de17fa
- R 620a97593d72a35202b30b5a287713e0
- U drh
- Z 9bdf30c70846e28b70673bedd2e84b73
 -P 2ea29c77a85236ca4126c05c7fd1d45a80dfe3f653af9b3ed86d6e62877ec5af
 -R a2fc8c0e7a0f6bea9f1def17bb732ac9
++P b2517d01e65b34ea4ca52c9149d7b255a36a45a50b332cb8ccfdacf22e629be2 1781e5e8d0cd2b508f7992987257416bd48841ea7e30bc9294784a7c1a402be7
++R fb2317e58ee4c6fd1b0a04dcff5b1916
++T +closed 1781e5e8d0cd2b508f7992987257416bd48841ea7e30bc9294784a7c1a402be7
+ U stephan
 -Z b4adf7c2382aa0d4cd736b4f6228cb1f
++Z 7997af5d18e22f187072ca02f9e440d9
  # Remove this line to create a well-formed Fossil manifest.
diff --cc manifest.uuid
index 1213867743a259cfcdfcc89343dfe328cb58d2ec,f8bdb35ab0f3bc2e7d8043fd2abc5d307cc40bda..22bbd0d9cc507644a4843634c18160daac5c000e
@@@ -1,1 -1,1 +1,1 @@@
- b2517d01e65b34ea4ca52c9149d7b255a36a45a50b332cb8ccfdacf22e629be2
 -1781e5e8d0cd2b508f7992987257416bd48841ea7e30bc9294784a7c1a402be7
++ec866b04d088e53b09764d00f5403902785328fd8f3cbf80d3affa166477711d