/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*fsl_vtbl_fossil_settings.c
/ *
/ *ver 1.0.0
/ *orig 10/12/2014 9:49:00 AM
/ *========================================================================
    
    fsl_vtbl_fossil_settings.c

    impl
    
========================================================================*/
#if defined(_MSC_VER)
#pragma warning ( disable : 4786 )
/* ^^^ "The identifier string exceeded the maximum allowable length and was truncated."
   Only affects debug info.
*/
#endif
#ifndef SQLITE_CORE
#include "sqlite3ext.h"
/*declares extern the vtable of all the sqlite3 functions (for loadable modules only)*/
SQLITE_EXTENSION_INIT3
#else
#include "sqlite3.h"
#endif

#include "fossil-vtbl_fossil_settings.h"

#include <stddef.h> /*really, just for NULL*/
#include <sys/stat.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#ifdef WIN32
#include <direct.h>
#else
#include <unistd.h>
#include <strings.h>
#endif

#include "fossil-ext_regexp.h"   /*sqlite3's regex engine*/
#include "fossil-scm/fossil-core.h"         /*for some public defs*/
#include "fossil-scm/fossil-util.h"         /*for some internal class defs*/


#ifndef COUNTOF
#define COUNTOF(arr) (sizeof(arr)/sizeof(*arr))
#endif


#if defined(_MSC_VER)
/*Microsoft C dropped POSIX, and only does ISO C++*/
#else
#define _stat stat
#define _unlink unlink
#define _mkdir mkdir
#define _stricmp strcasecmp
#define _strlwr strlwr
#endif

/*we emulate class derivation with aggregation; helper macro to downcast to a concrete type*/
#define DOWNCAST(derivedtype,basemember,ptr) ((derivedtype*)(((unsigned char*)ptr)-offsetof(derivedtype,basemember)))

/*====================================================================
  the 'SettingsMetadata' virtual table is a read-only table of info about
  the public, well-known fossil settings, as per 'fossil settings'.  It
  is exposed as a sqlite3 virtual table to facilitate joining.

  In addition to having interesting data, it can facilitate working
  around the absence of a full outer join, and allow computing 'effective
  settings' via a coalesce.  (It works around the full outer join
  limitation by letting the metadata be the leftmost table in a series
  of left outer joins).
*/


/*descriptor of a well-known (publicly facing) setting in fossil*/
struct tagSettingDesc {
  const char* _pszName;           /*setting name*/
  bool        _bIsVersionable;    /*can it be in .fossil-settings?*/
  const char* _pszDefault;        /*the default value, if any*/
  const char* _pszHelp;           /*Help text, tooltip*/
  int         _nHelpTextID;       /*id of Help text (for eventual i18n)*/
  bool        _bIsMultivalue;     /*should there be list processing?*/
  const char* _pszRegexAccept;    /*regex to validate input value*/
};
typedef struct tagSettingDesc SettingDesc;



/*common validation regexes*/
/*Note: we have created some 'extended' metacharacters beyond those supported
by the regex engine proper:
\$i  regex is case-insensitive
\$m  regex is for a multi-valued setting (e.g. 'ignore-glob')
\$e  regex should match the empty setting as well
these metacharacters must be at the beginning of the regex, because we handle
their processing programattically, and use the rest of the string to compile
as per usual.
These metacharacters are needed principally because MATCH_OR_FAIL doesn't
have context of the setting it is validating, to get to the metadata for that
setting.  I could have made MATCH_OR_FAIL also take the setting name, so that
it can find the metatdata, but this way it is also generally useful for other
non-setting-related data validation purposes.
*/
static const char sg_pszreNonvalidated[] = ".*";
static const char sg_pszreBasicMultivalue[] = "\\$e\\$m^\\s*[^,\n\r]*\\s*$";
static const char sg_pszreBoolean[] = "\\$e\\$i^\\s*(true|false|1|0|on|off|yes|no)\\s*$";
static const char sg_pszreNumber[] = "\\$e^\\s*\\d+\\s*$";
static const char sg_pszreDecNumber[] = "\\$e^\\s*(\\d+\\.?\\d*)\\s*|\\s*(\\d*\\.?\\d+)\\s*$";
static const char sg_pszrePermissions[] = "^[abcdefghijklmnopqrstuvwxz]*$";



/*Note, these MUST be in alpha order by name, because we do binary searches*/
/*over the collection*/
static const SettingDesc g_asdSettingsDescriptors[] = {
  {
    "access-log",
    false,
    "off",
    "If enabled, record successful and failed login attempts "
    "in the \"accesslog\" table.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "allow-symlinks",
    true,
    "off",
    "If enabled, don't follow symlinks, and instead treat "
    "them as symlinks on Unix. Has no effect on Windows "
    "(existing links in repository created on Unix become "
    "plain-text files with link destination path inside).",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "auto-captcha",
    false,
    "on",
    "If enabled, the Login page provides a button to "
    "fill in the captcha password.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "auto-hyperlink",
    false,
    "on",
    "Use javascript to enable hyperlinks on web pages "
    "for all users (regardless of the \"h\" privilege) if the "
    "User-Agent string in the HTTP header look like it came "
    "from real person, not a spider or bot.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "auto-shun",
    false,
    "on",
    "If enabled, automatically pull the shunning list "
    "from a server to which the client autosyncs.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "autosync",
    false,
    "on",
    "If enabled, automatically pull prior to commit "
    "or update and automatically push after commit or "
    "tag or branch creation.  If the value is \"pullonly\" "
    "then only pull operations occur automatically.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "binary-glob",
    true,
    "",
    "The VALUE is a comma or newline-separated list of "
    "GLOB patterns that should be treated as binary files "
    "for committing and merging purposes.  Example: *.jpg ",
    0,
    true,
    sg_pszreBasicMultivalue
  },
  {
    "case-sensitive",
    false,
    "false",
    "If TRUE, the files whose names differ only in case "
    "are considered distinct.  If FALSE files whose names "
    "differ only in case are the same file.\r\n"
    "Defaults to TRUE for unix and FALSE for Cygwin, Mac and Windows.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "clean-glob",
    true,
    "",
    "The VALUE is a comma or newline-separated list of GLOB "
    "patterns specifying files that the \"clean\" command will "
    "delete without prompting even when the -force flag has "
    "not been used.  Example:  *.a *.lib *.o",
    0,
    true,
    sg_pszreBasicMultivalue
  },
  {
    "clearsign",
    false,
    "off",
    "When enabled, fossil will attempt to sign all commits "
    "with gpg.  When disabled (the default), commits will "
    "be unsigned.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "crnl-glob",
    true,
    "",
    "A comma or newline-separated list of GLOB patterns for "
    "text files in which it is ok to have CR, CR+NL or mixed "
    "line endings. Set to \"*\" to disable CR+NL checking.",
    0,
    true,
    sg_pszreBasicMultivalue
  },
  {
    "default-perms",
    false,
    "u",
    "Permissions given automatically to new users.  For more "
    "information on permissions see Users page in Server "
    "Administration of the HTTP UI.",
    0,
    false,
    sg_pszrePermissions
  },
  {
    "diff-binary",
    false,
    "true",
    "If TRUE (the default), permit files that may be binary "
    "or that match the \"binary-glob\" setting to be used with "
    "external diff programs.  If FALSE, skip these files.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "diff-command",
    false,
    "",
    "External command to run when performing a diff.\r\n"
    "If undefined, the internal text diff will be used.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "dont-push",
    false,
    "false",
    "Prevent this repository from pushing from client to "
    "server.  Useful when setting up a private branch.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "editor",
    false,
#ifdef WIN32
    "notepad.exe",
#elif defined ( __APPLE__ ) || defined ( __OSX__ )
    "",     /*YYY verify*/
#else
    "",     /*YYY verify*/
#endif
    "Text editor command used for check-in comments.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "empty-dirs",
    true,
    "",
    "A comma or newline-separated list of pathnames. On "
    "update and checkout commands, if no file or directory "
    "exists with that name, an empty directory will be "
    "created.",
    0,
    true,
    sg_pszreBasicMultivalue
  },
  {
    "encoding-glob",
    true,
    "",
    "The VALUE is a comma or newline-separated list of GLOB "
    "patterns specifying files that the \"commit\" command will "
    "ignore when issuing warnings about text files that may "
    "use another encoding than ASCII or UTF-8. Set to \"*\" "
    "to disable encoding checking.",
    0,
    true,
    sg_pszreBasicMultivalue
  },
  {
    "gdiff-command",
    false,
#ifdef WIN32
    "WinDiff.exe",
#elif defined ( __APPLE__ ) || defined ( __OSX__ )
    "",     /*YYY verify*/
#else
    "",     /*YYY verify*/
#endif
    "External command to run when performing a graphical "
    "diff. If undefined, text diff will be used.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "gmerge-command",
    false,
    "",
    "A graphical merge conflict resolver command operating "
    "on four files.\r\n"
    "Ex: kdiff3 \"%baseline\" \"%original\" \"%merge\" -o \"%output\"\r\n"
    "Ex: xxdiff \"%original\" \"%baseline\" \"%merge\" -M \"%output\"\r\n"
    "Ex: meld \"%baseline\" \"%original\" \"%merge\" \"%output\"",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "http-port",
    false,
    "8080",
    "The TCP/IP port number to use by the \"server\""
    "and \"ui\" commands.",
    0,
    false,
    sg_pszreNumber
  },
  {
    "https-login",
    false,
    "",
    "Send login credentials using HTTPS instead of HTTP "
    "even if the login page request came via HTTP.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "ignore-glob",
    true,
    "",
    "The VALUE is a comma or newline-separated list of GLOB "
    "patterns specifying files that the \"add\", \"addremove\", "
    "\"clean\", and \"extra\" commands will ignore.\r\n"
    "Example:  *.log customCode.c notes.txt",
    0,
    true,
    sg_pszreBasicMultivalue
  },
  {
    "keep-glob",
    true,
    "",
    "The VALUE is a comma or newline-separated list of GLOB "
    "patterns specifying files that the \"clean\" command will "
    "keep.",
    0,
    true,
    sg_pszreBasicMultivalue
  },
  {
    "localauth",
    false,
    "false",
    "If enabled, require that HTTP connections from "
    "127.0.0.1 be authenticated by password.  If "
    "false, all HTTP requests from localhost have "
    "unrestricted access to the repository.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "main-branch",
    false,
    "trunk",
    "The primary branch for the project.",
    0,
    false,
    sg_pszreNonvalidated  /*YYY need regex*/
  },
  {
    "manifest",
    true,
    "off",
    "If enabled, automatically create files \"manifest\" and "
    "\"manifest.uuid\" in every checkout.  The SQLite and "
    "Fossil repositories both require this.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "max-loadavg",
    false,
    "0.0",
    "Some CPU-intensive web pages (ex: /zip, /tarball, /blame) "
    "are disallowed if the system load average goes above this "
    "value.  \"0.0\" means no limit.  This only works on unix.\r\n"
    "Only local settings of this value make a difference since "
    "when running as a web-server, Fossil does not open the "
    "global configuration database.",
    0,
    false,
    sg_pszreDecNumber
  },
  {
    "max-upload",
    false,
    "250000",
    "A limit on the size of uplink HTTP requests.  The "
    "default is 250000 bytes.",
    0,
    false,
    sg_pszreNumber
  },
  {
    "mtime-changes",
    false,
    "on",
    "Use file modification times (mtimes) to detect when "
    "files have been modified.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "pgp-command",
    false,
    "gpg --clearsign -o ",
    "Command used to clear-sign manifests at check-in.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "proxy",
    false,
    "",
    "URL of the HTTP proxy.  If undefined or \"off\" then "
    "the \"http_proxy\" environment variable is consulted.\r\n"
    "If the http_proxy environment variable is undefined "
    "then a direct HTTP connection is used.",
    0,
    false,
    sg_pszreNonvalidated    /*YYY could/should be better?*/
  },
  {
    "relative-paths",
    false,
    "on",
    "When showing changes and extras, report paths relative "
    "to the current working directory.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "repo-cksum",
    false,
    "on",
    "Compute checksums over all files in each checkout "
    "as a double-check of correctness.  Defaults to \"on\". "
    "Disable on large repositories for a performance "
    "improvement.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "self-register",
    false,
    "off",
    "Allow users to register themselves through the HTTP UI.\r\n"
    "This is useful if you want to see other names than "
    "\"Anonymous\" in e.g. ticketing system. On the other hand "
    "users can not be deleted.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "ssh-command",
    false,
    "",
    "Command used to talk to a remote machine with "
    "the \"ssh://\" protocol.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "ssl-ca-location",
    false,
    "",
    "The full pathname to a file containing PEM encoded "
    "CA root certificates, or a directory of certificates "
    "with filenames formed from the certificate hashes as "
    "required by OpenSSL.\r\n"
    "If set, this will override the OS default list of "
    "OpenSSL CAs. If unset, the default list will be used.\r\n"
    "Some platforms may add additional certificates.\r\n"
    "Check your platform behaviour is as required if the "
    "exact contents of the CA root is critical for your "
    "application.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "ssl-identity",
    false,
    "",
    "The full pathname to a file containing a certificate "
    "and private key in PEM format. Create by concatenating "
    "the certificate and private key files.\r\n"
    "This identity will be presented to SSL servers to "
    "authenticate this client, in addition to the normal "
    "password authentication.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "tcl",
    false,
    "off",
    "If enabled (and Fossil was compiled with Tcl support), "
    "Tcl integration commands will be added to the TH1 "
    "interpreter, allowing arbitrary Tcl expressions and "
    "scripts to be evaluated from TH1.  Additionally, the Tcl "
    "interpreter will be able to evaluate arbitrary TH1 "
    "expressions and scripts.",
    0,
    false,
    sg_pszreBoolean
  },
  {
    "tcl-setup",
    true,
    "",
    "This is the setup script to be evaluated after creating "
    "and initializing the Tcl interpreter.  By default, this "
    "is empty and no extra setup is performed.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "th1-setup",
    true,
    "",
    "This is the setup script to be evaluated after creating "
    "and initializing the TH1 interpreter.  By default, this "
    "is empty and no extra setup is performed.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "th1-uri-regexp",
    true,
    "",
    "Specify which URI's are allowed in HTTP requests from "
    "TH1 scripts.  If empty, no HTTP requests are allowed "
    "whatsoever.  The default is an empty string.",
    0,
    false,
    sg_pszreNonvalidated
  },
  {
    "web-browser",
    false,
#ifdef WIN32
    "start",
#elif defined ( __APPLE__ ) || defined ( __OSX__ )
    "open",
#else
    "firefox",
#endif
    "A shell command used to launch your preferred "
    "web browser when given a URL as an argument.\r\n"
    "Defaults to \"start\" on windows, \"open\" on Mac, "
    "and \"firefox\" on Unix.",
    0,
    false,
    sg_pszreNonvalidated
  },
};

static const size_t g_asdSettingsDescriptorsCount = COUNTOF(g_asdSettingsDescriptors);




/*===========================================
/ *our objects for settings metadata v-table
*/

struct tagvtblSetMetDat_table {
  sqlite3_vtab    _base;
  /*const sqlite3_module *pModule;    / *The module for this virtual table*/
  /*int nRef;                         / *NO LONGER USED*/
  /*char *zErrMsg;                    / *Error message from sqlite3_mprintf()*/
  /*Virtual table implementations will typically add additional fields*/
};
typedef struct tagvtblSetMetDat_table vtblSetMetDat_table;


static vtblSetMetDat_table* vtblSetMetDat_table_new ( void ) {
  vtblSetMetDat_table* pThis = (vtblSetMetDat_table*) fsl_malloc ( sizeof(vtblSetMetDat_table) );
  /*any ctor actions...*/
  /*YYY (BBB docs say lib will set these up, but it doesn't.  it does setup pModule, however)*/
  if(pThis){
    pThis->_base.nRef = 0;          /*unused, but we clear it anyway*/
    pThis->_base.zErrMsg = NULL;    /*ensure null lest there be crashes when lib tries to free*/
  }
  return pThis;
}

static void vtblSetMetDat_table_delete ( vtblSetMetDat_table* pThis ) {
  if ( NULL != pThis )
  {
    /*any dtor actions...*/
    fsl_free ( pThis );
  }
}



struct tagvtblSetMetDat_cursor {
  sqlite3_vtab_cursor     _base;
  /*sqlite3_vtab *pVtab;      / *Virtual table of this cursor*/
  /*Virtual table implementations will typically add additional fields*/
  size_t _nIdxEntry;
};
typedef struct tagvtblSetMetDat_cursor vtblSetMetDat_cursor;


static vtblSetMetDat_cursor* vtblSetMetDat_cursor_new() {
  vtblSetMetDat_cursor* pThis = (vtblSetMetDat_cursor*) fsl_malloc ( sizeof(vtblSetMetDat_cursor) );
  if(pThis){
    /*(any ctor actions...)*/
    pThis->_nIdxEntry = 0;
  }
  return pThis;
}


static void vtblSetMetDat_cursor_delete ( vtblSetMetDat_cursor* pThis ) {
  if ( NULL != pThis ) {
    /*(any dtor actions...)*/
    fsl_free ( pThis );
  }
}



/*===========================================*/
/*sqlite vtable interface code*/
/*'table' stuff*/



/*xCreate -- 'create virtual table' called; make a sqlite3_vtab*/
static int vtblSetMetDat_Create ( sqlite3* db, void* pAux, int argc, const char*const* argv, sqlite3_vtab** ppVTab, char** pzErr ) {
  /*argv[0]   / *name of module*/
  /*argv[1]   / *name of database; "main", "temp", ...*/
  /*argv[2]   / *name of the virtual table*/
  /*argv[3...]    / *arguments of the sql create virtual table statement; if present*/
    
  /*first, declare our effective schema*/
  int ret = sqlite3_declare_vtab ( db, "CREATE TABLE [ignored] ( "
      "NAME TEXT,"                /*setting name*/
      "ISVERSIONABLE INTEGER,"    /*can it be in .fossil-settings*/
      "DEFAULT_VALUE TEXT,"       /*the default value, if any*/
      "VALUE TEXT HIDDEN,"        /*the default value, aliased to 'value'*/
      "HELP TEXT,"                /*Help text, tooltip*/
      "HELP_ID INTEGER,"          /*id of Help text (for eventual i18n)*/
      "ISMULTIVALUE INTEGER,"     /*should it be interpreted as a list of values*/
      "REGEX_ACCEPT TEXT"         /*regex to validate input value*/
      ")" );
  if ( SQLITE_OK != ret )
    return ret;
    
  /*anything else we need?*/
  *ppVTab = &vtblSetMetDat_table_new()->_base;
    
  return SQLITE_OK;
}



/*xDestroy -- drop virtual table; the last one, so global cleanup can be done*/
/*last connection to this table is going away*/
static int vtblSetMetDat_Destroy ( sqlite3_vtab* pVTab ) {
  vtblSetMetDat_table* pThis = DOWNCAST(vtblSetMetDat_table,_base,pVTab);
  vtblSetMetDat_table_delete ( pThis );
  return SQLITE_OK;
}



/*xConnect -- connect to an existing vtable; relevant for dealing with idempotently instantiated state, otherwise the same as xCreate   */
/*new connection to existing table; we just treat it the same as 'create'*/
static int vtblSetMetDat_Connect ( sqlite3* db, void* pAux, int argc, const char*const* argv, sqlite3_vtab** ppVTab, char** pzErr )
{ return vtblSetMetDat_Create ( db, pAux, argc, argv, ppVTab, pzErr ); }


/*xDisconnect -- drop virtual table, but not the last one*/
/*table is going away, but not last one*/
/*(BBB actually I have seen the last one go away via this method in the*/
/*case of database closure when there was not an explicit drop table)*/
static int vtblSetMetDat_Disconnect ( sqlite3_vtab* pVTab )
{ return vtblSetMetDat_Destroy ( pVTab ); }


/*xBestIndex -- prepare for query, indicate what filtering we can do efficiently ourselves*/
/*think about what query is being done, and indicate what possible internal*/
/*quasi-index we might have*/
static int vtblSetMetDat_BestIndex ( sqlite3_vtab* pVTab, sqlite3_index_info* pIdxInfo ) {
  /*
  vtblSetMetDat_table* pThis = DOWNCAST(vtblSetMetDat_table,_base,pVTab);
  int nIdx;
  / *Inputs* /
  pIdxInfo->nConstraint;                      / *Number of entries in aConstraint* /
  pIdxInfo->aConstraint[0].iColumn;           / *Column on left-hand side of constraint* /
  pIdxInfo->aConstraint[0].op;                / *Constraint operator* /
  pIdxInfo->aConstraint[0].usable;            / *True if this constraint is usable* /
  pIdxInfo->aConstraint[0].iTermOffset;       / *Used internally - xBestIndex should ignore* /
  pIdxInfo->nOrderBy;                         / *Number of terms in the ORDER BY clause* /
  pIdxInfo->aOrderBy[0].iColumn;              / *Column number* /
  pIdxInfo->aOrderBy[0].desc;                 / *True for DESC.  False for ASC.* /
  / *Outputs* /
  pIdxInfo->aConstraintUsage[0].argvIndex;    / *if >0, constraint is part of argv to xFilter* /
  pIdxInfo->aConstraintUsage[0].omit;         / *Do not code a test for this constraint* /
  pIdxInfo->idxNum;                           / *Number used to identify the index* /
  pIdxInfo->idxStr;                           / *String, possibly obtained from sqlite3_malloc* /
  pIdxInfo->needToFreeIdxStr;                 / *Free idxStr using sqlite3_free() if true* /
  pIdxInfo->orderByConsumed;                  / *True if output is already ordered* /
  pIdxInfo->estimatedCost;                    / *Estimated cost of using this index* /
  / *Fields below are only available in SQLite 3.8.2 and later* /
  if ( sqlite3_libversion_number() >= 3008002 ) {
    pIdxInfo->estimatedRows;                  / *Estimated number of rows returned* /
  }

  / *whizz through the contraints, looking for ones we can do ourselves, setting* /
  / *stuff up that will later be sent to xFilter* /
  for ( nIdx = 0; nIdx < pIdxInfo->nConstraint; ++nIdx ) {
    if ( pIdxInfo->aConstraint[0].usable ) {
    }
  }

  / *see if we can do order by ourselves more efficiently
  */
  return SQLITE_OK;
}


/*===========================================*/
/*'cursor' stuff*/


/*xOpen -- open a cursor*/
static int vtblSetMetDat_cursor_Open ( sqlite3_vtab* pVTab, sqlite3_vtab_cursor** ppCursor ) {
  *ppCursor = &vtblSetMetDat_cursor_new()->_base;
  return SQLITE_OK;
}


/*xClose -- close a cursor*/
static int vtblSetMetDat_cursor_Close ( sqlite3_vtab_cursor* pCur ) {
  vtblSetMetDat_cursor* pThis = DOWNCAST(vtblSetMetDat_cursor,_base,pCur);
  vtblSetMetDat_cursor_delete ( pThis );
  return SQLITE_OK;
}



static int vtblSetMetDat_cursor_Filter ( sqlite3_vtab_cursor* pCur, int idxNum, const char* idxStr, int argc, sqlite3_value** argv ) {
  vtblSetMetDat_cursor* pThis = DOWNCAST(vtblSetMetDat_cursor,_base,pCur);
  /*position cursor to location based on value arguments and parameters that ultimately came from xBestIndex*/
  /*this will 'rewind' the cursor to the beginning*/
  pThis->_nIdxEntry = 0;
  return SQLITE_OK;
}



/*xNext -- advance a cursor*/
static int vtblSetMetDat_cursor_Next ( sqlite3_vtab_cursor* pCur ) {
  vtblSetMetDat_cursor* pThis = DOWNCAST(vtblSetMetDat_cursor,_base,pCur);
  /*advance a cursor*/
  /*we iterate through the list*/
  if ( pThis->_nIdxEntry != g_asdSettingsDescriptorsCount ) {   /*still some left*/
    ++pThis->_nIdxEntry;    /*next*/
    return SQLITE_OK;
  }

  return SQLITE_OK;
}



/*xEof -- check for end of scan*/
static int vtblSetMetDat_cursor_Eof ( sqlite3_vtab_cursor* pCur ) {
  vtblSetMetDat_cursor* pThis = DOWNCAST(vtblSetMetDat_cursor,_base,pCur);
  /*check for end of iteration*/
  return ( g_asdSettingsDescriptorsCount == pThis->_nIdxEntry ) ? 1 : 0;
}



/*xColumn -- read data*/
static int vtblSetMetDat_cursor_Column ( sqlite3_vtab_cursor* pCur, sqlite3_context* pCtx, int nCol ) {
  vtblSetMetDat_cursor* pThis = DOWNCAST(vtblSetMetDat_cursor,_base,pCur);
  const SettingDesc* psd;
  /*read data*/
    
  if ( g_asdSettingsDescriptorsCount == pThis->_nIdxEntry ) {   /*shouldn't happen; I don't think*/
    sqlite3_result_error ( pCtx, "reading past end vtblSetMetDat_cursor", -1 );
    return SQLITE_ERROR;
  }
    
  psd = &g_asdSettingsDescriptors[pThis->_nIdxEntry];
  switch ( nCol ) {
    /*"NAME TEXT,"              / *setting name*/
    case 0:
      sqlite3_result_text ( pCtx, psd->_pszName, -1, SQLITE_STATIC );
    break;
    /*"ISVERSIONABLE INTEGER,"  / *can it be in .fossil-settings*/
    case 1:
      sqlite3_result_int ( pCtx, psd->_bIsVersionable ? 1 : 0 );
    break;
    /*"DEFAULT_VALUE TEXT,"     / *the default value, if any*/
    /*"VALUE TEXT,"             / *the default value, aliased to 'value'*/
    case 2:
    case 3:
      if ( NULL == psd->_pszDefault || '\0' == psd->_pszDefault[0] )
        sqlite3_result_null ( pCtx );
      else
        sqlite3_result_text ( pCtx, psd->_pszDefault, -1, SQLITE_STATIC );
    break;
    /*HELP TEXT,"               / *Help text, tooltip*/
    case 4:
      if ( NULL == psd->_pszHelp || '\0' == psd->_pszHelp[0] )
        sqlite3_result_null ( pCtx );
      else
        sqlite3_result_text ( pCtx, psd->_pszHelp, -1, SQLITE_STATIC );
    break;
    /*"HELP_ID INTEGER,"            / *id of Help text (for eventual i18n)*/
    case 5:
      sqlite3_result_int ( pCtx, psd->_nHelpTextID );
    break;
    /*"ISMULTIVALUE INTEGER,"     / *should it be interpreted as a list of values*/
    case 6:
      sqlite3_result_int ( pCtx, psd->_bIsMultivalue ? 1 : 0 );
    break;
    /*"REGEX_ACCEPT TEXT"           / *regex to validate input value*/
    case 7:
      if ( NULL == psd->_pszRegexAccept || '\0' == psd->_pszRegexAccept[0] )
        sqlite3_result_null ( pCtx );
      else
        sqlite3_result_text ( pCtx, psd->_pszRegexAccept, -1, SQLITE_STATIC );
    break;
    default:    /*que?*/
      sqlite3_result_null ( pCtx );
    break;
  }
  return SQLITE_OK;
}


/*xRowid -- read data*/
static int vtblSetMetDat_cursor_Rowid ( sqlite3_vtab_cursor* pCur, sqlite3_int64* pRowid ) {
  vtblSetMetDat_cursor* pThis = DOWNCAST(vtblSetMetDat_cursor,_base,pCur);
  /*read data*/
  *pRowid = pThis->_nIdxEntry;
  return SQLITE_OK;
}




static const sqlite3_module sg_pvtblSettingsMetadata = {
  2,                          /*iVersion -- version of this structure, so sqlite doesn't access past end*/
  vtblSetMetDat_Create,       /*xCreate -- 'create virtual table' called; make a sqlite3_vtab*/
  vtblSetMetDat_Connect,      /*xConnect -- connect to an existing vtable; relevant for dealing with idempotently instantiated state, otherwise the same as xCreate*/
  vtblSetMetDat_BestIndex,    /*xBestIndex -- prepare for query, indicate what filtering we can do efficiently ourselves*/
  vtblSetMetDat_Disconnect,   /*xDisconnect -- drop virtual table, but not the last one*/
  vtblSetMetDat_Destroy,      /*xDestroy -- drop virtual table; the last one, so global cleanup can be done*/
  vtblSetMetDat_cursor_Open,  /*xOpen -- open a cursor*/
  vtblSetMetDat_cursor_Close, /*xClose -- close a cursor*/
  vtblSetMetDat_cursor_Filter,    /*xFilter -- configure scan constraints*/
  vtblSetMetDat_cursor_Next,  /*xNext -- advance a cursor*/
  vtblSetMetDat_cursor_Eof,   /*xEof -- check for end of scan*/
  vtblSetMetDat_cursor_Column,    /*xColumn -- read data*/
  vtblSetMetDat_cursor_Rowid, /*xRowid -- read data*/
  /*(this table is read-only, so we don't implement the rest of these)*/
  NULL,                       /*int (*xUpdate)(sqlite3_vtab *, int, sqlite3_value **, sqlite3_int64 *);*/
  NULL,                       /*int (*xBegin)(sqlite3_vtab *pVTab);*/
  NULL,                       /*int (*xSync)(sqlite3_vtab *pVTab);*/
  NULL,                       /*int (*xCommit)(sqlite3_vtab *pVTab);*/
  NULL,                       /*int (*xRollback)(sqlite3_vtab *pVTab);*/
  NULL,                       /*int (*xFindFunction)(sqlite3_vtab *pVtab, int nArg, const char *zName, void (**pxFunc)(sqlite3_context*,int,sqlite3_value**), void **ppArg);*/
  NULL,                       /*int (*xRename)(sqlite3_vtab *pVtab, const char *zNew);*/
  /*v2 methods*/
  NULL,                       /*int (*xSavepoint)(sqlite3_vtab *pVTab, int);*/
  NULL,                       /*int (*xRelease)(sqlite3_vtab *pVTab, int);*/
  NULL,                       /*int (*xRollbackTo)(sqlite3_vtab *pVTab, int);*/
};



/*================================================*/
/*versioned settings vtable*/
/*this allows us to treat 'versioned settings' (settings that stored in*/
/*files in a .fossil-settings directory, and checked in) as a sqlite*/
/*table, like all the other settings.  This keeps things uniform, and*/
/*can facilitate joining them.*/


/*===========================================*/
/*our objects for versioned settings v-table*/


/*since there are only a handful of 'versioned' settings that are supported,*/
/*and since the ones that are, are small textual values, we will just make*/
/*our life easier here, and read them all into memory at once.*/


/*for multi-valued types (e.g. 'ignore-glob'), we attempt to detect and
preserve the style of the original file to minimize diffs*/
typedef enum StyleValueSeparation {
  SEPSTYLE_UNKNOWN = 0,   /*couldn't figure it out; probably because there were no multi-values*/
  SEPSTYLE_NEWLINE = 1,
  SEPSTYLE_COMMA = 2,
  SEPSTYLE_NONE = 3       /*this type is known to not be multivalue; inhibit list processing*/
} StyleValueSeparation;


/*we attempt to detect and preserve the newline style of the original
file to minimize diffs*/
typedef enum tagStyleLineEnding {
  NLSTYLE_UNKNOWN = 0,    /*couldn't figure it out; probably because there were no newlines*/
  NLSTYLE_CRLF = 1,   /*DOS/Windows*/
  NLSTYLE_CR = 2,     /*Mac*/
  NLSTYLE_LF = 3      /*Unices*/
} StyleLineEnding;



#if defined(WIN32) || defined(WINDOWS) || defined(DOS)
static const char sg_chPathSep = '\\';
#else
static const char sg_chPathSep = '/';
#endif



struct tagSettingInfo {
  fsl_buffer              _strName;           /*name*/
  fsl_buffer              _strValue;          /*value, normalized to CSV*/
  fsl_buffer              _strOrigValue;      /*backup of value, when in xactn*/
  bool                    _bDirty;            /*if _strValue contains unwritten changes*/
  bool                    _bNew;              /*if this setting was inserted*/
  /*We try to figure out the separators from the file itself, so we can*/
  /*preserve those choices when persisting back out.  This is done as a*/
  /*courtesy, to avoid creating unnecessary diffs.*/
  StyleValueSeparation    _esvsSep;           /*what was the original value separation style*/
  StyleLineEnding         _esleNewline;       /*what was the original 'newline' style*/
  fsl_buffer              _strOriginalPath;   /*what file contained this setting*/
  /*synthesized properties*/
  sqlite_int64            _nRowId;            /*sqlite needs this, and it can only be int64*/
};
typedef struct tagSettingInfo SettingInfo;


static void SettingInfo_ctor ( SettingInfo* pThis ) {
  pThis->_strName = fsl_buffer_empty;         /*name*/
  pThis->_strValue = fsl_buffer_empty;        /*value, normalized to CSV*/
  pThis->_strOrigValue = fsl_buffer_empty;    /*backup of value, when in xactn*/
  pThis->_bDirty = false;                     /*if _strValue contains unwritten changes*/
  pThis->_bNew = false;                       /*if this setting was inserted*/
  /*We try to figure out the separators from the file itself, so we can*/
  /*preserve those choices when persisting back out.  This is done as a*/
  /*courtesy, to avoid creating unnecessary diffs.*/
  pThis->_esvsSep = SEPSTYLE_UNKNOWN;         /*what was the original value separation style*/
  pThis->_esleNewline = NLSTYLE_UNKNOWN;      /*what was the original 'newline' style*/
  pThis->_strOriginalPath = fsl_buffer_empty; /*what file contained this setting*/
  /*synthesized properties*/
  pThis->_nRowId = 0;         /*sqlite needs this, and it can only be int64*/
}


static void SettingInfo_dtor ( SettingInfo* pThis ) {
  fsl_buffer_clear( &pThis->_strName );
  fsl_buffer_clear( &pThis->_strValue );
  fsl_buffer_clear( &pThis->_strOrigValue );
  fsl_buffer_clear( &pThis->_strOriginalPath );
}



static SettingInfo* SettingInfo_new ( void ) {
  SettingInfo* pThis = (SettingInfo*) fsl_malloc ( sizeof(SettingInfo) );
  if( pThis ){
    SettingInfo_ctor ( pThis );
  }
  return pThis;
}



static void SettingInfo_delete ( SettingInfo* pThis ) {
  if ( NULL != pThis ) {
    SettingInfo_dtor ( pThis );
    fsl_free ( pThis );
  }
}



static int fsl_list_visitor_free ( void* obj, void* visitorState ) {
  fsl_free ( obj );
  return 0;
}



static SettingInfo* SettingInfo_createFromFile ( const char* pszDir, const SettingDesc* pdesc ) {
  /*Note to self; this impl was originally in C++, where it (maybe) made a*/
  /*little more sense, however a tidier impl comes to mind:*/
  /**  read the whole file into buffer*/
  /**  characterise the styles from that buffer*/
  /**  strpbrk out the comma, cr, lf -> nul*/
  /*      list_append the ptr into the buffer of the start of the value text*/
  /**  finally, transform the value into csv*/
  /*this would avoid a lot of buffer copies and dtor code*/
	/*YYY Note:  fsl_buffer_fill_from_filename can help*/
	/*YYY Note:  fsl_glob_list_parse/fsl_glob_list_clear can help*/
  StyleValueSeparation esvsSep;
  StyleLineEnding esleNewline;
  fsl_buffer strPath;
  struct _stat buf;
  fsl_buffer strValue;
  SettingInfo* psi;

  strPath = fsl_buffer_empty;
  fsl_buffer_appendf ( &strPath, "%s%c%s", pszDir, sg_chPathSep, pdesc->_pszName );
    
  /*if the file does not exist, we can't try to depersist*/
  if ( 0 != _stat ( fsl_buffer_cstr ( &strPath ), &buf ) ) {
    fsl_buffer_clear ( &strPath );
    return NULL;
  }

  /*figure out the 'style' of this file*/
  esvsSep = SEPSTYLE_UNKNOWN;
  esleNewline = NLSTYLE_UNKNOWN;
  {
    FILE* f;
    char ch;
    size_t nRead;
    bool bEOF;
    bool bCommaFound;
    char chFirstLineTerm;

    /*open the file*/
    f = fopen ( fsl_buffer_cstr ( &strPath ), "rb" );
    if ( NULL == f ) {
      /*horror*/
      fsl_buffer_clear ( &strPath );
      return NULL;
    }
    /*read (and discard) a line, character by character, looking for interesting things*/
    bCommaFound = false;
    chFirstLineTerm = '\0';
    while ( 
        nRead = fread ( &ch, sizeof(ch), 1, f ),
        bEOF = ( sizeof(ch) != nRead ),
        bCommaFound |= ch == ',',
        chFirstLineTerm = ( ch == '\r' ||ch == '\n' ) ? ch : chFirstLineTerm,
        ! bEOF && ! chFirstLineTerm
        ) {
    }
    
    /*If we saw a comma, then surely it is comma style. If we didn't */
    /*see a comma, but saw a newline, then we will consider it it*/
    /*newline style. Otherwise, all bets are off.*/
    esvsSep = bCommaFound ? SEPSTYLE_COMMA : 
        ( chFirstLineTerm ? SEPSTYLE_NEWLINE : SEPSTYLE_UNKNOWN );

    /*if we saw a first line term, try to get the next char, which will seal*/
    /*the deal as to what kind of line terms we have*/
    if ( ! bEOF ) {
      nRead = fread ( &ch, sizeof(ch), 1, f );
      bEOF = ( sizeof(ch) != nRead );
      if ( bEOF ) {
        /*only one line term and only one line*/
        if ( '\r' == chFirstLineTerm )
          esleNewline = NLSTYLE_CR;
        else if ( '\n' == chFirstLineTerm )
          esleNewline = NLSTYLE_LF;
        else
        {}  /*unknown; impossible*/
      }
      else {
        /*maybe two line terms, or maybe start a new line*/
        char chNextLineTerm = ( ch == '\r' || ch == '\n' ) ? ch : '\0';
        if ( chNextLineTerm ) {
          /*two line terms*/
          if ( '\r' == chFirstLineTerm && '\n' == chNextLineTerm )
            esleNewline = NLSTYLE_CRLF;
          if ( '\r' == chFirstLineTerm && '\r' == chNextLineTerm )
            esleNewline = NLSTYLE_CR;
          if ( '\n' == chFirstLineTerm && '\r' == chNextLineTerm )
            esleNewline = NLSTYLE_CRLF; /*absurd*/
          else if ( '\n' == chFirstLineTerm  && '\n' == chNextLineTerm )
            esleNewline = NLSTYLE_LF;
          else
          {}  /*unknown; impossible*/
        }
        else {
          /*only one line term; started a new line*/
          if ( '\r' == chFirstLineTerm )
            esleNewline = NLSTYLE_CR;
          else if ( '\n' == chFirstLineTerm )
            esleNewline = NLSTYLE_LF;
          else {
          }   /*unknown; impossible*/
        }
      }
    }

    fclose ( f );
  }
  /*well, OK, if metadata says we're not multivalue, override any inferred sep style*/
  if ( ! pdesc->_bIsMultivalue ) {
    esvsSep = SEPSTYLE_NONE;
  }
  
  /*now, open file, read lines, parse seps, build string list, then*/
  /*anneal string list into csv*/
  strValue = fsl_buffer_empty;
  if ( SEPSTYLE_NONE == esvsSep ) {
    if ( FSL_RC_OK != fsl_buffer_fill_from_filename ( &strValue,
        fsl_buffer_cstr ( &strPath ) ) ) {
      /*absurd; we just opened it a moment ago*/
      fsl_buffer_clear ( &strValue );
      fsl_buffer_clear ( &strPath );
      return NULL;
    }
  }
  else {
    /*YYY reimplement this using the fsl_glob_list_parse()*/
    fsl_list lstrValues;
    fsl_buffer strToken;
    char ch;
    size_t nRead;
    bool bEOF;
    FILE* f;
    fsl_size_t nIdx;

    lstrValues = fsl_list_empty;
    strToken = fsl_buffer_empty;

    f = fopen ( fsl_buffer_cstr ( &strPath ), "rb" );
    if ( NULL == f ) {
      /*absurd; we just opened it a moment ago*/
      fsl_buffer_clear ( &strToken );
      fsl_list_reserve ( &lstrValues, 0 );
      fsl_buffer_clear ( &strValue );
      fsl_buffer_clear ( &strPath );
      return NULL;
    }
    
    while ( 
        nRead = fread ( &ch, sizeof(ch), 1, f ),
        bEOF = ( sizeof(ch) != nRead ),
        ! bEOF
        ) {
      if ( ',' == ch || '\r' == ch || '\n' == ch ) {
        if ( 0 != fsl_buffer_size ( &strToken ) ) { /*push token*/
          fsl_list_append ( &lstrValues, fsl_strdup ( fsl_buffer_cstr ( &strToken ) ) );
          fsl_buffer_reset ( &strToken );
        }
      }
      else    /*build token*/
        fsl_buffer_append ( &strToken, &ch, sizeof(ch) );
    }
    if ( 0 != fsl_buffer_size ( &strToken ) ) { /*a last one? (like if file ended without final sep)*/
      fsl_list_append ( &lstrValues, fsl_strdup ( fsl_buffer_cstr ( &strToken ) ) );
      fsl_buffer_reset ( &strToken );
    }

    fclose ( f );

    /*normalize into CSV*/
    for ( nIdx = 0; nIdx < lstrValues.used; ++nIdx ) {
      char chComma = ',';
      if (  0 != fsl_buffer_size ( &strValue ) )
        fsl_buffer_append ( &strValue, &chComma, sizeof(chComma) );
      fsl_buffer_append ( &strValue, lstrValues.list[nIdx], fsl_strlen ( lstrValues.list[nIdx] ) );
    }

    /*dtor*/
    fsl_buffer_clear ( &strToken );
    fsl_list_clear ( &lstrValues, fsl_list_visitor_free, NULL );
  }

  /*OK, now we can get down to business...*/
  psi = SettingInfo_new();
  fsl_buffer_append ( &psi->_strName, pdesc->_pszName, fsl_strlen ( pdesc->_pszName ) );
  fsl_buffer_append ( &psi->_strOriginalPath, fsl_buffer_cstr ( &strPath ), fsl_buffer_size ( &strPath ) );
  psi->_esvsSep = esvsSep;
  psi->_esleNewline = esleNewline;
  fsl_buffer_append ( &psi->_strValue, fsl_buffer_cstr ( &strValue ), fsl_buffer_size ( &strValue ) );
  psi->_nRowId = 0;           /*(parent will set this up)*/
    
  fsl_buffer_clear ( &strValue );
  fsl_buffer_clear ( &strPath );
    
  return psi;
}



static SettingInfo* SettingInfo_createFromParams ( const char* pszDir, const char* pszName,
      const char* pszValue, StyleValueSeparation esvsSep, StyleLineEnding esleNewline ) {
  fsl_buffer strPath;
  SettingInfo* psi;

  strPath = fsl_buffer_empty;
  fsl_buffer_appendf ( &strPath, "%s%c%s", pszDir, sg_chPathSep, pszName );

  /**/
  psi = SettingInfo_new();
  fsl_buffer_append ( &psi->_strName, pszName, -1 );
  fsl_buffer_append ( &psi->_strOriginalPath, fsl_buffer_cstr ( &strPath ), fsl_buffer_size ( &strPath ) );
  psi->_esvsSep = esvsSep;
  psi->_esleNewline = esleNewline;
  if ( NULL != pszValue )
    fsl_buffer_append ( &psi->_strValue, pszValue, -1 );
  psi->_nRowId = 0;

  fsl_buffer_clear ( &strPath );
  return psi;
}



static bool SettingInfo_saveToFile ( const SettingInfo* pThis ) {

  if ( SEPSTYLE_NONE == pThis->_esvsSep ) {
    /*this is not a multivar; just emit the data verbatim*/
    if ( FSL_RC_OK != fsl_buffer_to_filename( &pThis->_strValue,
        fsl_buffer_cstr ( &pThis->_strOriginalPath ) ) ) {
      return false;
    }
  }
  else {
    FILE* f;
    size_t nIdx;

    f = fopen ( fsl_buffer_cstr ( &pThis->_strOriginalPath ), "wb" );
    if ( NULL == f ) {
      return false;
    }
    /*emit characters in the string until we hit a ',' which we will*/
    /*translate into separators/newline according to style.*/
    for ( nIdx = 0; nIdx < fsl_buffer_size ( &pThis->_strValue ); ++nIdx ) {
      if ( ',' == pThis->_strValue.mem[nIdx] ) {
        if ( SEPSTYLE_COMMA == pThis->_esvsSep ) {
          char ch = ',';
          fwrite ( &ch, sizeof(ch), 1, f );
        }
        else {
          if ( NLSTYLE_CRLF == pThis->_esleNewline ) {
            char ach[2] = { '\r', '\n' };
            fwrite ( ach, sizeof(ach), 1, f );
          }
          else if ( NLSTYLE_CR == pThis->_esleNewline ) {
            char ch = '\r';
            fwrite ( &ch, sizeof(ch), 1, f );
          }
          else {  /*NLSTYLE_LF*/
            char ch = '\n';
            fwrite ( &ch, sizeof(ch), 1, f );
          }
        }
      }
      else {
        fwrite ( &pThis->_strValue.mem[nIdx], sizeof(pThis->_strValue.mem[nIdx]), 1, f );
      }
    }

  fclose ( f );
  }

  return true;
}



static bool SettingInfo_removeFile ( const SettingInfo* pThis ) {
  return 0 == _unlink ( fsl_buffer_cstr ( &pThis->_strOriginalPath ) );
}



/*our setting collection is a map of 'rowid's to settings info*/
/*since we only have a few known 'versioned settings', I am just going to use*/
/*an array, and do a linear search for my finds.*/
struct tagSettingCollTuple {
  sqlite_int64    _nRowId;
  SettingInfo*    _psi;
};
typedef struct tagSettingCollTuple SettingCollTuple;

static int fsl_list_visitor_settingtuple_free ( void* obj, void* visitorState ) {
  SettingCollTuple* ptpl = (SettingCollTuple*)obj;
  /*dtor on the tuple members*/
  /*ptpl->_nRowId*/
  SettingInfo_delete ( ptpl->_psi );
  /*delete the tuple*/
  fsl_free ( ptpl );
  return 0;
}


struct tagvtblVerSet_table {
  sqlite3_vtab    _base;
  /*const sqlite3_module *pModule;    / *The module for this virtual table*/
  /*int nRef;                         / *NO LONGER USED*/
  /*char *zErrMsg;                    / *Error message from sqlite3_mprintf()*/
  /*Virtual table implementations will typically add additional fields*/

  sqlite3*  _db;  /*the database to which we are attached*/

  /*the platform-defined styles are used when inserting new settings, or*/
  /*when we could not determine them from the file (e.g. one element, no line term)*/
  StyleValueSeparation    _esvsSepPlat;       /*value separator default, as per platform*/
  StyleLineEnding         _esleNewlinePlat;   /*newline default, as per platform*/
    
  fsl_buffer              _strVerSettingsPath;    /*the .fossil-setting directory; normalized to NO trailing path sep*/
  sqlite_int64            _nLastRowId;        /*where our rowids come from*/
    
  fsl_list                _settings;          /*the settings, depersisted from files*/
  fsl_list                _deletedSettings;   /*uncommitted deleted settings*/

  int     _nCursorsOpen;  /*count of open cursors; some updating cannot happen with > 0*/
  /*YYY need (linked) list to facilitate updates for stability under deletes;
    but I have punted here, and deletes are forbidden with open cursors*/
};
typedef struct tagvtblVerSet_table vtblVerSet_table;


static void vtblVerSet_table_ctor ( vtblVerSet_table* pThis, sqlite3* db, const char* pszVerSettingsPath ) {
  /*YYY (BBB docs say lib will set these up, but it doesn't.  it does setup pModule, however)*/
  pThis->_base.nRef = 0;          /*unused, but we clear it anyway*/
  pThis->_base.zErrMsg = NULL;    /*ensure null lest there be crashes when lib tries to free*/
  pThis->_db = db;  /*we need this from time-to-time*/
  /*platform-specific defaults for multi-value and newline styles.  These*/
  /*are used in cases where we couldn't figure them out, like, if there*/
  /*was only one element*/
  pThis->_esvsSepPlat = SEPSTYLE_NEWLINE;
#if defined(WIN32) || defined(WINDOWS) || defined(DOS)
  pThis->_esleNewlinePlat = NLSTYLE_CRLF; /*use windowsian CRLF*/
#else
  pThis->_esleNewlinePlat = NLSTYLE_LF;   /*fall back to unixian LF*/
#endif
  pThis->_strVerSettingsPath = fsl_buffer_empty;
  fsl_buffer_append ( &pThis->_strVerSettingsPath, pszVerSettingsPath, fsl_strlen ( pszVerSettingsPath ) );
  pThis->_nLastRowId = 0;
  pThis->_settings = fsl_list_empty;
  pThis->_deletedSettings = fsl_list_empty;
  pThis->_nCursorsOpen = 0;
}



static void vtblVerSet_table_dtor ( vtblVerSet_table* pThis ) {
  fsl_list_clear ( &pThis->_settings, fsl_list_visitor_settingtuple_free, NULL );
  fsl_list_clear ( &pThis->_deletedSettings, fsl_list_visitor_settingtuple_free, NULL );
  fsl_buffer_clear( &pThis->_strVerSettingsPath );
}



static vtblVerSet_table* vtblVerSet_table_new ( sqlite3* db, const char* pszVerSettingsPath ) {
  vtblVerSet_table* pThis = (vtblVerSet_table*) fsl_malloc ( sizeof(vtblVerSet_table) );
  if( pThis ){
    vtblVerSet_table_ctor ( pThis, db, pszVerSettingsPath );
  }
  return pThis;
}



static void vtblVerSet_table_delete ( vtblVerSet_table* pThis ) {
  if ( NULL != pThis ) {
    vtblVerSet_table_dtor ( pThis );
    fsl_free ( pThis );
  }
}



static bool vtblVerSet_table_addExistingSetting ( vtblVerSet_table* pThis, const SettingDesc* pdesc ) {
  /*factory idiom*/
  SettingInfo* psi = SettingInfo_createFromFile ( fsl_buffer_cstr ( &pThis->_strVerSettingsPath ), pdesc );
  if ( NULL != psi ) {
    SettingCollTuple* psct;
    if ( psi->_esvsSep == SEPSTYLE_UNKNOWN )
      psi->_esvsSep = pThis->_esvsSepPlat;
    if ( psi->_esleNewline == NLSTYLE_UNKNOWN )
      psi->_esleNewline = pThis->_esleNewlinePlat;
    psi->_nRowId = ++(pThis->_nLastRowId);
    /**/
    psct = (SettingCollTuple*) fsl_malloc ( sizeof(SettingCollTuple) );
    if(psct){
      if( 0==fsl_list_append ( &pThis->_settings, psct ) ){
        psct->_nRowId = psi->_nRowId;
        psct->_psi = psi;
      }else{
        fsl_free(psct);
        psct = 0;
      }
    }
    if(!psct){
      SettingInfo_delete(psi);
      psi = 0;
    }
  }
  return NULL != psi;
}



static bool vtblVerSet_table_populate ( vtblVerSet_table* pThis ) {
  size_t nIdx;
  for ( nIdx = 0; nIdx < g_asdSettingsDescriptorsCount; ++nIdx ) {
    if ( g_asdSettingsDescriptors[nIdx]._bIsVersionable )
      vtblVerSet_table_addExistingSetting ( pThis, &g_asdSettingsDescriptors[nIdx] );
  }
  return true;
}



/*since there are only a few well-known versionable settings, I am simply going*/
/*to do a linear search for 'find' operations.  The 'find' operation will*/
/*return the index into the _settings array (er, 'list').  You may use this*/
/*index to get at the setting object, or use it to erase the setting (and*/
/*manage the list correctly).  Non-found items will return an index of -1.*/
static int vtblVerSet_table_find_setting_idx_by_rowid ( vtblVerSet_table* pThis, sqlite3_int64 nRowId ) {
  int nIdx;
  for ( nIdx = 0; nIdx < (int) pThis->_settings.used; ++nIdx ) {
    if ( ((SettingCollTuple*)(pThis->_settings.list[nIdx]))->_nRowId == nRowId )
      return nIdx;
  }
  return -1;
}



static int implFindSettingIdxByName ( fsl_list* plist, const char* pszName ) {
  int nIdx;
  for ( nIdx = 0; nIdx < (int) plist->used; ++nIdx ) {
    if ( 0 == _stricmp ( fsl_buffer_cstr (
        &((SettingCollTuple*)(plist->list[nIdx]))->_psi->_strName ),
        pszName ) )
      return nIdx;
  }
  return -1;
}


static int vtblVerSet_table_find_setting_idx_by_name ( 
    vtblVerSet_table* pThis, const char* pszName ) {
  return implFindSettingIdxByName ( &pThis->_settings, pszName );
}



static int vtblVerSet_table_find_deleted_setting_idx_by_name ( 
    vtblVerSet_table* pThis, const char* pszName ) {
  return implFindSettingIdxByName ( &pThis->_deletedSettings, pszName );
}


/*helper to remove and destroy an item in a given settings list*/
static void implEraseSettingListByIdx ( fsl_list* plist, int nIdx ) {
  SettingCollTuple* ptpl;
  if ( nIdx < 0 || nIdx >= (int) plist->used )
    return; /*bogus; assert-worthy*/
  /*delete the tuple*/
  ptpl = (SettingCollTuple*)(plist->list[nIdx]);
  fsl_list_visitor_settingtuple_free ( ptpl, NULL );
  /*scootch memory and update counts*/
  memmove ( &plist->list[nIdx],
      &plist->list[nIdx+1],
      (size_t) ( plist->used - nIdx - 1 ) * sizeof(plist->list[0]) );
  --(plist->used);
}


/*remove and destroy an item in the settings collection*/
static void vtblVerSet_table_erase_setting_by_idx ( vtblVerSet_table* pThis, int nIdx ) {
  implEraseSettingListByIdx ( &pThis->_settings, nIdx );
}


/*remove and destroy an item in the deleted settings collection*/
/*unused; here for doc
static void vtblVerSet_table_erase_deleted_setting_by_idx ( vtblVerSet_table* pThis, int nIdx ) {
  implEraseSettingListByIdx ( &pThis->_deletedSettings, nIdx );
}
*/


/*helper to transfer a settings list item from one list to another, by append*/
static void implTransferSettingListByIdx ( 
    fsl_list* plistDest, fsl_list* plistSrc, int nIdxSrc ) {
  SettingCollTuple* ptpl;
  if ( nIdxSrc < 0 || nIdxSrc >= (int) plistSrc->used )
    return; /*bogus; assert-worthy*/
  /*transfer the tuple to the dest list*/
  ptpl = (SettingCollTuple*)(plistSrc->list[nIdxSrc]);
  fsl_list_append ( plistDest, ptpl );
  /*scootch memory and update counts*/
  memmove ( &plistSrc->list[nIdxSrc],
      &plistSrc->list[nIdxSrc+1],
      (size_t) ( plistSrc->used - nIdxSrc - 1 ) * sizeof(plistSrc->list[0]) );
  --(plistSrc->used);
}



static void implTransferReplaceSettingListByIdx ( 
    fsl_list* plistDest, int nIdxDest, fsl_list* plistSrc, int nIdxSrc ) {
  SettingCollTuple* ptplDest, * ptplSrc;
  if ( nIdxDest < 0 || nIdxDest >= (int) plistDest->used )
    return; /*bogus; assert-worthy*/
  if ( nIdxSrc < 0 || nIdxSrc >= (int) plistSrc->used )
    return; /*bogus; assert-worthy*/
  /*perform surgery on the two tuples to transfer content appropriately*/
  /*delete the tuple*/
  ptplDest = (SettingCollTuple*)(plistDest->list[nIdxDest]);
  ptplSrc = (SettingCollTuple*)(plistSrc->list[nIdxSrc]);
  {
  sqlite3_int64 nTemp;
  SettingInfo* psiTemp;
  nTemp = ptplDest->_nRowId; ptplDest->_nRowId = ptplSrc->_nRowId; ptplSrc->_nRowId = nTemp;
  psiTemp = ptplDest->_psi; ptplDest->_psi = ptplSrc->_psi; ptplSrc->_psi = psiTemp;
  }
  /*destroy this husk of an object*/
  fsl_list_visitor_settingtuple_free ( ptplSrc, NULL );
  /*scootch memory and update counts*/
  memmove ( &plistSrc->list[nIdxSrc],
      &plistSrc->list[nIdxSrc+1],
      (size_t) ( plistSrc->used - nIdxSrc - 1 ) * sizeof(plistSrc->list[0]) );
  --(plistSrc->used);
}


struct tagvtblVerSet_cursor {
  sqlite3_vtab_cursor _base;
  /*sqlite3_vtab *pVtab;      / *Virtual table of this cursor*/
  /*Virtual table implementations will typically add additional fields*/
  size_t  _nIter;             /*where in the collection are we*/
  vtblVerSet_table* _pvst;    /*(strictly, this is redundant with base member pVtab)*/
  /*anything else we need?*/
};
typedef struct tagvtblVerSet_cursor vtblVerSet_cursor;



static void vtblVerSet_cursor_ctor ( vtblVerSet_cursor* pThis, vtblVerSet_table* pvst ) {
  pThis->_nIter = 0;
  pThis->_pvst = pvst;
  ++(pThis->_pvst->_nCursorsOpen);
}



static void vtblVerSet_cursor_dtor ( vtblVerSet_cursor* pThis ) {
  --(pThis->_pvst->_nCursorsOpen);
}



static vtblVerSet_cursor* vtblVerSet_cursor_new ( vtblVerSet_table* pvst ) {
  vtblVerSet_cursor* pThis = (vtblVerSet_cursor*) fsl_malloc ( sizeof(vtblVerSet_cursor) );
  if( pThis ){
    vtblVerSet_cursor_ctor ( pThis, pvst );
  }
  return pThis;
}



static void vtblVerSet_cursor_delete ( vtblVerSet_cursor* pThis ) {
  if ( NULL != pThis ) {
    vtblVerSet_cursor_dtor ( pThis );
    fsl_free ( pThis );
  }
}



/*===========================================*/
/*sqlite vtable interface code*/
/*'table' stuff*/



/*xCreate -- 'create virtual table' called; make a sqlite3_vtab*/
static int vtblVerSet_Create ( sqlite3* db, void* pAux, int argc, const char*const* argv, sqlite3_vtab** ppVTab, char** pzErr ) {
  int nLen;
  char* pszValue;
  fsl_buffer strVerSettingsPath;
  char ch;
  bool bMkdirOK;
  int nIdx;
  struct _stat buf;
  int ret;
  vtblVerSet_table* pThis;

  /*argv[0]   / *name of module*/
  /*argv[1]   / *name of database; "main", "temp", ...*/
  /*argv[2]   / *name of the virtual table*/
  /*argv[3...]    / *arguments of the sql create virtual table statement; if present*/
  if ( argc < 4 ) {   /*must have first argument; path to .fossil-settings directory*/
    *pzErr = sqlite3_mprintf ( "VERSIONED_SETTINGS requires first parameter of path to .fossil-settings directory" );
    return SQLITE_ERROR;    /*if you return error, you must set pzErr to something or NULL*/
  }

  /*first arg is the fully-qualified path to the .fossil-settings directory*/
  strVerSettingsPath = fsl_buffer_empty;
  /*knock off the SQL quotes*/
  pszValue = (char*) argv[3];
  nLen = fsl_strlen ( pszValue );
  if ( 0 != nLen && '\'' == pszValue[0] ) {
    ++pszValue;
    --nLen;
  }
  if ( 0 != nLen && '\'' == pszValue[nLen-1] ) {
    --nLen;
  }
  fsl_buffer_append ( &strVerSettingsPath, pszValue, nLen );
  /*nibble off trailing path seps*/
  {
    pszValue = fsl_buffer_str ( &strVerSettingsPath );
    while ( nLen > 0 &&
        ( ch = pszValue[nLen-1],
        '\\' == ch || '/' == ch ) ) {
      --nLen;
      pszValue[nLen] = '\0';
    }
    strVerSettingsPath.used = nLen;
  }

  /*whizz through the rest of the optional parameters*/
  bMkdirOK = false;
  if ( argc > 4 ) {
    for ( nIdx = 4; nIdx < argc; ++nIdx ) {
      pszValue = (char*) argv[nIdx];
      if ( 0 == _stricmp ( "MKDIR", pszValue ) ) {
        bMkdirOK = true;  /*we can create the .fossil-settings dir if we need to*/
      } else {
        /*unrecognized option is an error*/
        *pzErr = sqlite3_mprintf ( "unrecognized option to VERSIONED_SETTINGS" );
        fsl_buffer_clear ( &strVerSettingsPath );
        return SQLITE_ERROR;    /*if you return error, you must set pzErr to something or NULL*/
      }
    }
  }

  /*apply 'mkdir' policy:  if the .fossil-setting directory does not exist, but
  we specified the 'MKDIR' option, then create the directory.  Otherwise, the
  absence of the directory is considered an error.*/
  {
    bool bDirExists = ( 0 == _stat ( fsl_buffer_cstr ( &strVerSettingsPath ), &buf ) );
    if ( ! bDirExists ) {
      if ( ! bMkdirOK ) {
        *pzErr = sqlite3_mprintf ( "VERSIONED_SETTINGS requires that the .fossil-settings directory already exists" );
        fsl_buffer_clear ( &strVerSettingsPath );
        return SQLITE_ERROR;    /*if you return error, you must set pzErr to something or NULL*/
      } else {
#ifdef WIN32
        if ( 0 != _mkdir ( fsl_buffer_cstr ( &strVerSettingsPath ) ) ) {
#else
        if ( 0 != _mkdir ( fsl_buffer_cstr ( &strVerSettingsPath ),
            S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH ) ) {
#endif
          *pzErr = sqlite3_mprintf ( "VERSIONED_SETTINGS attempted to create the versioned setting directory, and failed.  Only the final path component maybe be created." );
          fsl_buffer_clear ( &strVerSettingsPath );
          return SQLITE_ERROR;    /*if you return error, you must set pzErr to something or NULL*/
        }
      }
    }
  }

  /*first, declare our effective schema*/
  ret = sqlite3_declare_vtab ( db, "CREATE TABLE [ignored] ( "
      "NAME TEXT,"                        /*setting name*/
      "VALUE TEXT,"                       /*setting value, if any*/
      "VALUE_SEPARATOR INTEGER HIDDEN,"   /*style of value separation*/
      "LINE_ENDINGS INTEGER HIDDEN,"      /*style of line endings*/
      "ORIGINAL_PATH TEXT HIDDEN"         /*path where this setting came from*/
      ")" );
  if ( SQLITE_OK != ret ) {
    fsl_buffer_clear ( &strVerSettingsPath );
    return ret;
  }

  /*now, create our virtual table object*/
  pThis = vtblVerSet_table_new ( db, fsl_buffer_cstr ( &strVerSettingsPath ) );
  /*populate it with settings coming from files*/
  if ( ! vtblVerSet_table_populate ( pThis ) ) {
    *pzErr = sqlite3_mprintf ( "could not populate versioned settings table from files" );
    vtblVerSet_table_delete ( pThis );  /*we must destroy this ourselves since we are returning an error*/
    fsl_buffer_clear ( &strVerSettingsPath );
    return SQLITE_ERROR;    /*if you return error, you must set pzErr to something or NULL*/
  }

  /*if it's all good, tell sqlite3 of what we made for it*/
  *ppVTab = &pThis->_base;

  /*we will support constraints, so that we can do INSERT OR REPLACE*/
  /*this also implies that we are forbidden to do any partial mods to
  underlying data in the event of conflict, but that's not a problem
  for us; we do all needed checking */
  sqlite3_vtab_config ( db, SQLITE_VTAB_CONSTRAINT_SUPPORT, 1 );

  fsl_buffer_clear ( &strVerSettingsPath );
  return SQLITE_OK;
}



/*xDestroy -- drop virtual table; the last one, so global cleanup can be done*/
/*last connection to this table is going away*/
static int vtblVerSet_Destroy ( sqlite3_vtab* pVTab ) {
  vtblVerSet_table* pThis = DOWNCAST(vtblVerSet_table,_base,pVTab);
  vtblVerSet_table_delete ( pThis );
  return SQLITE_OK;
}



/*xConnect -- connect to an existing vtable; relevant for dealing with idempotently instantiated state, otherwise the same as xCreate*/
/*new connection to existing table; we just treat it the same as 'create'*/
static int vtblVerSet_Connect ( sqlite3* db, void* pAux, int argc, const char*const* argv, sqlite3_vtab** ppVTab, char** pzErr ) {
  return vtblVerSet_Create ( db, pAux, argc, argv, ppVTab, pzErr );
}



/*xDisconnect -- drop virtual table, but not the last one*/
/*table is going away, but not last one*/
/*(BBB actually I have seen the last one go away via this method in the*/
/*case of database closure when there was not an explicit drop table)*/
static int vtblVerSet_Disconnect ( sqlite3_vtab* pVTab ) {
  return vtblVerSet_Destroy ( pVTab );
}


/*xBestIndex -- prepare for query, indicate what filtering we can do efficiently ourselves*/
/*think about what query is being done, and indicate what possible internal*/
/*quasi-index we might have*/
static int vtblVerSet_BestIndex ( sqlite3_vtab* pVTab, sqlite3_index_info* pIdxInfo ) {
  /*vtblVerSet_table* pThis = DOWNCAST(vtblVerSet_table,_base,pVTab);
  int nIdx;
  / *Inputs* /
  pIdxInfo->nConstraint;                      / *Number of entries in aConstraint* /
  pIdxInfo->aConstraint[0].iColumn;           / *Column on left-hand side of constraint* /
  pIdxInfo->aConstraint[0].op;                / *Constraint operator* /
  pIdxInfo->aConstraint[0].usable;            / *True if this constraint is usable* /
  pIdxInfo->aConstraint[0].iTermOffset;       / *Used internally - xBestIndex should ignore* /
  pIdxInfo->nOrderBy;                         / *Number of terms in the ORDER BY clause* /
  pIdxInfo->aOrderBy[0].iColumn;              / *Column number* /
  pIdxInfo->aOrderBy[0].desc;                 / *True for DESC.  False for ASC.* /
  / *Outputs* /
  pIdxInfo->aConstraintUsage[0].argvIndex;    / *if >0, constraint is part of argv to xFilter* /
  pIdxInfo->aConstraintUsage[0].omit;         / *Do not code a test for this constraint* /
  pIdxInfo->idxNum;                           / *Number used to identify the index* /
  pIdxInfo->idxStr;                           / *String, possibly obtained from sqlite3_malloc* /
  pIdxInfo->needToFreeIdxStr;                 / *Free idxStr using sqlite3_free() if true* /
  pIdxInfo->orderByConsumed;                  / *True if output is already ordered* /
  pIdxInfo->estimatedCost;                    / *Estimated cost of using this index* /
  / *Fields below are only available in SQLite 3.8.2 and later* /
  if ( sqlite3_libversion_number() >= 3008002 ) {
    pIdxInfo->estimatedRows;                  / *Estimated number of rows returned* /
  }

  / *whizz through the contraints, looking for ones we can do ourselves, setting* /
  / *stuff up that will later be sent to xFilter* /
  for ( nIdx = 0; nIdx < pIdxInfo->nConstraint; ++nIdx ) {
    if ( pIdxInfo->aConstraint[0].usable ) {
    }
  }

  / *see if we can do order by ourselves more efficiently
  */
  return SQLITE_OK;
}



/*===========================================*/
/*'cursor' stuff*/



/*xOpen -- open a cursor*/
static int vtblVerSet_cursor_Open ( sqlite3_vtab* pVTab, sqlite3_vtab_cursor** ppCursor ) {
  vtblVerSet_table* pThis = DOWNCAST(vtblVerSet_table,_base,pVTab);
  *ppCursor = &vtblVerSet_cursor_new ( pThis )->_base;
  return SQLITE_OK;
}



/*xClose -- close a cursor*/
static int vtblVerSet_cursor_Close ( sqlite3_vtab_cursor* pCur ) {
  vtblVerSet_cursor* pThis = DOWNCAST(vtblVerSet_cursor,_base,pCur);
  vtblVerSet_cursor_delete ( pThis );
  return SQLITE_OK;
}


static int vtblVerSet_cursor_Filter ( sqlite3_vtab_cursor* pCur, int idxNum, const char* idxStr, int argc, sqlite3_value** argv ) {
  vtblVerSet_cursor* pThis = DOWNCAST(vtblVerSet_cursor,_base,pCur);
  /*position cursor to location based on value arguments and parameters that ultimately came from xBestIndex*/
  /*this will 'rewind' the cursor to the beginning*/
  pThis->_nIter = 0;
  return SQLITE_OK;
}



/*xNext -- advance a cursor*/
static int vtblVerSet_cursor_Next ( sqlite3_vtab_cursor* pCur ) {
  vtblVerSet_cursor* pThis = DOWNCAST(vtblVerSet_cursor,_base,pCur);
  /*advance a cursor*/
  /*we iterate through the list*/
  if ( pThis->_nIter < pThis->_pvst->_settings.used ) {   /*still some left*/
    ++pThis->_nIter;    /*next*/
    return SQLITE_OK;
  }

  return SQLITE_OK;
}



/*xEof -- check for end of scan*/
static int vtblVerSet_cursor_Eof ( sqlite3_vtab_cursor* pCur ) {
  vtblVerSet_cursor* pThis = DOWNCAST(vtblVerSet_cursor,_base,pCur);
  /*check for end of iteration*/
  return (  pThis->_nIter >= pThis->_pvst->_settings.used  ) ? 1 : 0;
}



/*xColumn -- read data*/
static int vtblVerSet_cursor_Column ( sqlite3_vtab_cursor* pCur, sqlite3_context* pCtx, int nCol ) {
  const SettingInfo* psi;
  vtblVerSet_cursor* pThis = DOWNCAST(vtblVerSet_cursor,_base,pCur);
  /*read data*/

  psi = ((SettingCollTuple*)pThis->_pvst->_settings.list[pThis->_nIter])->_psi;
  switch ( nCol ) {
    /*"NAME TEXT,"              / *setting name*/
    case 0:
      sqlite3_result_text ( pCtx, fsl_buffer_cstr ( &psi->_strName ), -1, SQLITE_STATIC );
    break;

    /*"VALUE TEXT,"                     / *setting value, if any*/
    case 1:
      sqlite3_result_text ( pCtx, fsl_buffer_cstr ( &psi->_strValue ), -1, SQLITE_STATIC );
    break;

    /*"VALUE_SEPARATOR INTEGER HIDDEN," / *style of value separation*/
    case 2:
      sqlite3_result_int ( pCtx, psi->_esvsSep );
    break;

    /*"LINE_ENDINGS INTEGER HIDDEN,"        / *style of line endings*/
    case 3:
      sqlite3_result_int ( pCtx, psi->_esleNewline );
    break;
            
    /*"ORIGINAL_PATH TEXT HIDDEN"           / *path where this setting came from*/
    case 4:
      sqlite3_result_text ( pCtx, fsl_buffer_cstr ( &psi->_strOriginalPath ), -1, SQLITE_STATIC );
    break;

    default:    /*que?*/
      sqlite3_result_null ( pCtx );
    break;
  }
  return SQLITE_OK;
}



/*xRowid -- read data*/
static int vtblVerSet_cursor_Rowid ( sqlite3_vtab_cursor* pCur, sqlite3_int64* pRowid ) {
  vtblVerSet_cursor* pThis = DOWNCAST(vtblVerSet_cursor,_base,pCur);
  /*read data*/
  *pRowid = ((SettingCollTuple*)pThis->_pvst->_settings.list[pThis->_nIter])->_nRowId;
  return SQLITE_OK;
}



/*================================================*/
/*updating methods*/



static const SettingDesc* impl_isVersionableSetting ( const char* pszName ) {
  size_t nIdx;
  for ( nIdx = 0; nIdx < g_asdSettingsDescriptorsCount; ++nIdx )     {
    if ( g_asdSettingsDescriptors[nIdx]._bIsVersionable )
      if ( 0 == _stricmp ( g_asdSettingsDescriptors[nIdx]._pszName, pszName ) )
        return &g_asdSettingsDescriptors[nIdx];
  }
  return NULL;
}



static int vtblVerSet_Update ( sqlite3_vtab* pVTab, int argc, sqlite3_value** argv, sqlite3_int64* pRowid ) {
  vtblVerSet_table* pThis = DOWNCAST(vtblVerSet_table,_base,pVTab);

  /*All changes to a virtual table are made using the xUpdate method.*/
  /*This one method can be used to insert, delete, or update.*/

  /*The argv[1] parameter is the rowid of a new row to be inserted into*/
  /*the virtual table. If argv[1] is an SQL NULL, then the implementation*/
  /*must choose a rowid for the newly inserted row. Subsequent argv[]*/
  /*entries contain values of the columns of the virtual table, in the*/
  /*order that the columns were declared. The number of columns will*/
  /*match the table declaration that the xConnect or xCreate method made*/
  /*using the sqlite3_declare_vtab() call. All hidden columns are included.*/

  /*When doing an insert without a rowid (argc>1, argv[1] is an SQL NULL),*/
  /*the implementation must set *pRowid to the rowid of the newly inserted*/
  /*row; this will become the value returned by the*/
  /*sqlite3_last_insert_rowid() function. Setting this value in all the*/
  /*other cases is a harmless no-op; the SQLite engine ignores the *pRowid*/
  /*return value if argc==1 or argv[1] is not an SQL NULL.*/

  /*Each call to xUpdate will fall into one of cases shown below. Note*/
  /*that references to argv[i] mean the SQL value held within the argv[i]*/
  /*object, not the argv[i] object itself.*/

  /*case of delete*/
  if ( 1 == argc ) {
    sqlite3_int64 nRowId;
    int nIdxSetting;
    SettingCollTuple* ptpl;
    int nIdxDeleted;
    /*argc = 1*/
    /*The single row with rowid equal to argv[0] is deleted. No insert occurs.*/

    if ( pThis->_nCursorsOpen > 0 ) {
      /*we can't have open cursors, because cursors hold a _settings*/
      /*index.  Our erase would destabilize that index.*/
      pThis->_base.zErrMsg = sqlite3_mprintf ( "cannot delete with open cursors" );
      return SQLITE_ERROR;
    }
    if ( SQLITE_NULL == sqlite3_value_type ( argv[0] ) ) {
      /*I'm not sure this is even possible*/
      pThis->_base.zErrMsg = sqlite3_mprintf ( "you must specify the rowid on delete" );
      return SQLITE_CONSTRAINT;
    }
    nRowId = sqlite3_value_int64 ( argv[0] );
    nIdxSetting = vtblVerSet_table_find_setting_idx_by_rowid ( pThis, nRowId );
    if ( nIdxSetting < 0 ) {    /*preposterous, but handled*/
      /*I think this is absurd, maybe assert-worthy; where could the rowid have come from?*/
      pThis->_base.zErrMsg = sqlite3_mprintf ( "invalid rowid in delete request" );
      return SQLITE_CONSTRAINT;
    }
    ptpl = (SettingCollTuple*)(pThis->_settings.list[nIdxSetting]);

    /*use the object to destroy underlying data*/
    /*we must update cursors for stability in the context of the delete; but I
      have punted, and we forbid open cursors on delete (at top of this fxn)*/
    /*for transaction; we transfer to the 'deleted' list, unless it is
      already there (in which case we discard)*/
    nIdxDeleted = vtblVerSet_table_find_deleted_setting_idx_by_name ( 
        pThis, fsl_buffer_cstr ( &ptpl->_psi->_strName ) );
    if ( nIdxDeleted < 0 ) {
      /*transfer to deleted list*/
      implTransferSettingListByIdx ( &pThis->_deletedSettings,
          &pThis->_settings, nIdxSetting );
    }
    else {
      /*discard it, since the original delete is already there*/
      vtblVerSet_table_erase_setting_by_idx ( pThis, nIdxSetting );
    }

    return SQLITE_OK;
  }
  /*case of 'insert or replace'*/
  else if ( argc > 1 && SQLITE_NULL == sqlite3_value_type ( argv[0] ) ) {
    const char* pszName;
    const char* pszValue;
    int nsvs;
    int nsle;
    fsl_buffer strName;
    const SettingDesc* psmd;
    int nIdxSetting = -1;
    bool bUpdateInstead = 0;
    char* pch;
    SettingInfo* psi = NULL;
    SettingCollTuple* psct;
    /*argc > 1 */
    /*argv[0] = NULL*/
    /*A new row is inserted with a rowid argv[1] and column values in argv[2]*/
    /*and following. If argv[1] is an SQL NULL, then a new unique rowid is*/
    /*generated automatically.*/

    /*sqlite provides values for all the columns, even if the user did not*/
    /*explicitly, supplying null for those which the user did not provide.*/
    /*"NAME TEXT,"                      / *setting name*/
    pszName = (const char*) sqlite3_value_text ( argv[2] );
    /*"VALUE TEXT,"                     / *setting value, if any*/
    pszValue = (const char*) sqlite3_value_text ( argv[3] );
    /*"VALUE_SEPARATOR INTEGER HIDDEN," / *style of value separation*/
    nsvs = SQLITE_NULL == sqlite3_value_type ( argv[4] ) ? 
        (int) pThis->_esvsSepPlat : sqlite3_value_int ( argv[4] );
    /*"LINE_ENDINGS INTEGER HIDDEN,"        / *style of line endings*/
    nsle = SQLITE_NULL == sqlite3_value_type ( argv[5] ) ? 
        (int) pThis->_esleNewlinePlat : sqlite3_value_int ( argv[5] );
    /*"ORIGINAL_PATH TEXT HIDDEN"           / *path where this setting came from*/
    /*you're never going to be able to set this, but we'll just ignore it if you try*/
    /*argv[6]*/

    /*first, some sanity checks on the data*/
    if ( SQLITE_NULL != sqlite3_value_type ( argv[1] ) ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "you cannot specify the rowid on insert" );
      return SQLITE_CONSTRAINT;
    }
    if ( NULL == pszName ) {    /*gotta have a name; it's our filename!*/
      pThis->_base.zErrMsg = sqlite3_mprintf ( "NAME must not be null" );
      return SQLITE_CONSTRAINT;
    }
    /*range check:  you may only insert one the 'sanctioned nine'*/
    strName = fsl_buffer_empty;
    fsl_buffer_append ( &strName, pszName, -1 );
    for ( pch = fsl_buffer_str ( &strName ); *pch; ++pch )
      *pch = tolower ( (int)*pch );
    if ( NULL == ( psmd = impl_isVersionableSetting ( fsl_buffer_cstr ( &strName ) ) ) ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "NAME must be one of the versionable settings" );
      fsl_buffer_clear ( &strName );
      return SQLITE_CONSTRAINT;
    }
    /*now, if metadata says we're not multivalue, that trumps any specified style*/
    if ( ! psmd->_bIsMultivalue ) {
      nsvs = SEPSTYLE_NONE;
    }
    /*'unique' constraint check; you cannot insert a new instance of an existing value*/
    if ( -1 != ( nIdxSetting = vtblVerSet_table_find_setting_idx_by_name ( pThis, fsl_buffer_cstr ( &strName ) ) ) ) {
      /*maybe.  maybe we change this insert into an update*/
      if (  SQLITE_REPLACE == sqlite3_vtab_on_conflict ( pThis->_db ) ) {
          bUpdateInstead = true;
      } else {
        pThis->_base.zErrMsg = sqlite3_mprintf ( "NAME has a unique constraint, and an existing value was found" );
        fsl_buffer_clear ( &strName );
        return SQLITE_CONSTRAINT_UNIQUE;
      }
    }
    /*range check:  nsvs must be a value enum value*/
    if ( nsvs < SEPSTYLE_NEWLINE || nsvs > SEPSTYLE_NONE ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "VALUE_SEPARATOR must be a valid value" );
      fsl_buffer_clear ( &strName );
      return SQLITE_CONSTRAINT;
    }
    /*range check:  nsle must be a value enum value*/
    if ( nsle < NLSTYLE_CRLF || nsvs > NLSTYLE_LF ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "LINE_ENDINGS must be a valid value" );
      fsl_buffer_clear ( &strName );
      return SQLITE_CONSTRAINT;
    }

    /*OK, at this point, we should be cleared for takeoff*/
    /*for transaction support; we save orignal value to backup (unless it's
      already there), and mark as 'dirty'.  Or, we create a new value, and
      mark it as 'new'.*/
    if ( bUpdateInstead ) {
      SettingCollTuple* ptpl = (SettingCollTuple*)(pThis->_settings.list[nIdxSetting]);
      if ( ptpl->_psi->_bDirty ) {
        /*we've already saved the original; do not overwrite it*/
      }
      else {
        /*get value into backup*/
        fsl_buffer_swap ( &ptpl->_psi->_strValue, &ptpl->_psi->_strOrigValue );
        ptpl->_psi->_bDirty = true;
      }
      fsl_buffer_reset ( &ptpl->_psi->_strValue );
      fsl_buffer_append ( &ptpl->_psi->_strValue, pszValue, -1 );
      /*only override these parameters if we explicity tried to do so*/
      if ( SQLITE_NULL != sqlite3_value_type ( argv[4] ) )
        ptpl->_psi->_esvsSep = (StyleValueSeparation)nsvs;
      if ( SQLITE_NULL != sqlite3_value_type ( argv[5] ) )
        ptpl->_psi->_esleNewline = (StyleLineEnding)nsle;
      psi = ptpl->_psi; /*remember for rest of fxn*/
    } else {
      /*make the new object*/
      psi = SettingInfo_createFromParams (
          fsl_buffer_cstr ( &pThis->_strVerSettingsPath ),
          fsl_buffer_cstr ( &strName ),
          pszValue,
          (StyleValueSeparation)nsvs,
          (StyleLineEnding)nsle );
      psi->_nRowId = ++pThis->_nLastRowId;
      psi->_bNew = true;
      /*stick it in the settings collection*/
      psct = (SettingCollTuple*) fsl_malloc ( sizeof(SettingCollTuple) );
      if(psct){
        if( 0==fsl_list_append ( &pThis->_settings, psct ) ){
          psct->_nRowId = psi->_nRowId;
          psct->_psi = psi;
        }else{
          fsl_free(psct);
          psct = NULL;
        }
      }
      if(!psct){
        SettingInfo_delete(psi);
        psi = NULL;
      }
    }

    /*tell sqlite about the rowid*/
    if ( psi ) {
      *pRowid = psi->_nRowId;
    }

    fsl_buffer_clear ( &strName );
    return psi ? SQLITE_OK : SQLITE_NOMEM;
  }
  /*case of 'update values'*/
  else if ( argc > 1 && SQLITE_NULL != sqlite3_value_type ( argv[0] ) &&
      sqlite3_value_int64 ( argv[0] ) == sqlite3_value_int64 ( argv[1] ) ) {
    sqlite3_int64 nRowId;
    int nIdxSetting;
    SettingCollTuple* ptpl;
    const char* pszName;
    const SettingDesc* psmd;
    const char* pszValue;
    int nsvs;
    int nsle;
    /*argc > 1 */
    /*argv[0] <> NULL */
    /*argv[0] = argv[1]*/
    /*The row with rowid argv[0] is updated with new values in argv[2] and*/
    /*following parameters.*/

    nRowId = sqlite3_value_int64 ( argv[0] );
    nIdxSetting = vtblVerSet_table_find_setting_idx_by_rowid ( pThis, nRowId );
    if ( nIdxSetting < 0 ) {    /*preposterous, but handled*/
      /*I think this is absurd, maybe assert-worthy; where could the rowid have come from?*/
      pThis->_base.zErrMsg = sqlite3_mprintf ( "invalid rowid in update request" );
      return SQLITE_CONSTRAINT;
    }
    ptpl = (SettingCollTuple*)(pThis->_settings.list[nIdxSetting]);

    /*"NAME TEXT,"                      / *setting name*/
    pszName = (const char*) sqlite3_value_text ( argv[2] );
    if ( NULL == pszName ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "NAME cannot be null" );
      return SQLITE_CONSTRAINT;
    }
    /*"VALUE TEXT,"                     / *setting value, if any*/
    pszValue = (const char*) sqlite3_value_text ( argv[3] );
    /*name cannot change*/
    if ( 0 != _stricmp ( pszName, fsl_buffer_cstr ( &ptpl->_psi->_strName ) ) ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "NAME cannot change" );
      return SQLITE_ERROR;
    }
    /*"VALUE_SEPARATOR INTEGER HIDDEN," / *style of value separation*/
    nsvs = SQLITE_NULL == sqlite3_value_type ( argv[4] ) ? 
        (int) pThis->_esvsSepPlat : sqlite3_value_int ( argv[4] );
    if ( NULL == ( psmd = impl_isVersionableSetting ( fsl_buffer_cstr ( &ptpl->_psi->_strName ) ) ) ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "NAME must be one of the versionable settings" );
      return SQLITE_CONSTRAINT;
    }
    /*now, if metadata says we're not multivalue, that trumps any specified style*/
    if ( ! psmd->_bIsMultivalue ) {
      nsvs = SEPSTYLE_NONE;
    }
    /*range check:  nsvs must be a value enum value*/
    if ( nsvs < SEPSTYLE_NEWLINE || nsvs > SEPSTYLE_NONE ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "VALUE_SEPARATOR must be a valid value" );
      return SQLITE_CONSTRAINT;
    }
    /*"LINE_ENDINGS INTEGER HIDDEN,"        / *style of line endings*/
    nsle = SQLITE_NULL == sqlite3_value_type ( argv[5] ) ? 
        (int) pThis->_esvsSepPlat : sqlite3_value_int ( argv[5] );
    /*range check:  nsle must be a value enum value*/
    if ( nsle < NLSTYLE_CRLF || nsvs > NLSTYLE_LF ) {
      pThis->_base.zErrMsg = sqlite3_mprintf ( "LINE_ENDINGS must be a valid value" );
      return SQLITE_CONSTRAINT;
    }
    /*"ORIGINAL_PATH TEXT HIDDEN"           / *path where this setting came from*/
    /*you're never going to be able to change this, but we'll just ignore it if you try*/

    /*make the changes*/
    /*for transaction; we save backing value transfer value and mark dirty*/
    if ( ptpl->_psi->_bDirty ) {
      /*we've already saved the original; do not overwrite it*/
    }
    else {
      /*get value into backup*/
      fsl_buffer_swap ( &ptpl->_psi->_strValue, &ptpl->_psi->_strOrigValue );
      ptpl->_psi->_bDirty = true;
    }
    fsl_buffer_reset ( &ptpl->_psi->_strValue );
    fsl_buffer_append ( &ptpl->_psi->_strValue, pszValue, -1 );
    /*only override these parameters if we explicity tried to do so*/
    if ( SQLITE_NULL != sqlite3_value_type ( argv[4] ) )
      ptpl->_psi->_esvsSep = (StyleValueSeparation)nsvs;
    if ( SQLITE_NULL != sqlite3_value_type ( argv[5] ) )
      ptpl->_psi->_esleNewline = (StyleLineEnding)nsle;

    return SQLITE_OK;
  }
  /*case of 'update rowid'; not supported*/
  else if ( argc > 1 && SQLITE_NULL != sqlite3_value_type ( argv[0] ) &&
      sqlite3_value_int64 ( argv[0] ) != sqlite3_value_int64 ( argv[1] ) ) {
    /*argc > 1 */
    /*argv[0] <> NULL */
    /*argv[0] <> argv[1]*/
    /*The row with rowid argv[0] is updated with rowid argv[1] and new*/
    /*values in argv[2] and following parameters. This will occur when an*/
    /*SQL statement updates a rowid, as in the statement:*/

    /*UPDATE table SET rowid=rowid+1 WHERE ...;*/

    pThis->_base.zErrMsg = sqlite3_mprintf ( "changing rowid not supported" );
    return SQLITE_ERROR;
  }

  /*The xUpdate method must return SQLITE_OK if and only if it is*/
  /*successful. If a failure occurs, the xUpdate must return an appropriate*/
  /*error code. On a failure, the pVTab->zErrMsg element may optionally be*/
  /*replaced with error message text stored in memory allocated from SQLite*/
  /*using functions such as sqlite3_mprintf() or sqlite3_malloc().*/

  /*If the xUpdate method violates some constraint of the virtual table*/
  /*(including, but not limited to, attempting to store a value of the wrong*/
  /*datatype, attempting to store a value that is too large or too small, or*/
  /*attempting to change a read-only value) then the xUpdate must fail with*/
  /*an appropriate error code.*/

  /*There might be one or more sqlite3_vtab_cursor objects open and in use*/
  /*on the virtual table instance and perhaps even on the row of the*/
  /*virtual table when the xUpdate method is invoked. The implementation*/
  /*of xUpdate must be prepared for attempts to delete or modify rows of*/
  /*the table out from other existing cursors. If the virtual table cannot*/
  /*accommodate such changes, the xUpdate method must return an error code.*/

  /*The xUpdate method is optional. If the xUpdate pointer in the*/
  /*sqlite3_module for a virtual table is a NULL pointer, then the virtual*/
  /*table is read-only.*/

  /*(this should be logically unreachable)*/
  return SQLITE_ERROR;
}



/*
Transaction support:
  The approach we take here, is for the settings objects to have some flags
  indicating 'dirty' and 'new', and backups of unmodified values, and a
  'deleted items' list.  These are maintained during update operations to
  keep a current view of the settings within the transaction.  At commit
  or rollback time, these are used to correctly transition the set to
  a state synced with disk content.
  A drawback of this approach is that it cannot be nested, and therefor
  would need to be reworked to support savepoints.
*/



/*begin transaction on virtual table*/
static int vtblVerSet_Begin ( sqlite3_vtab* pVTab )
{
  /*(I don't think we really need to do anything special here)*/
  return SQLITE_OK;
}


/*notification of impending commit/rollback; for realizing two-phase commit*/
static int vtblVerSet_Sync ( sqlite3_vtab* pVTab )
{
  /*(I don't think we really need to do anything special here)*/
  return SQLITE_OK;
}


/*end a transaction on a virtual table, making changes effective*/
static int vtblVerSet_Commit ( sqlite3_vtab* pVTab )
{
  vtblVerSet_table* pThis = DOWNCAST(vtblVerSet_table,_base,pVTab);
  int nIdx;

  /*whizz through all settings; and create 'new' ones, and write out 'dirty' ones*/
  for ( nIdx = 0; nIdx < (int) pThis->_settings.used; ++nIdx ) {
    SettingCollTuple* ptpl = (SettingCollTuple*)(pThis->_settings.list[nIdx]);
    if ( ptpl->_psi->_bNew ) {
      if ( ! SettingInfo_saveToFile( ptpl->_psi ) ) {
        pThis->_base.zErrMsg = sqlite3_mprintf ( "failed to save value" );
        return SQLITE_ERROR;
      }
    }
    else if ( ptpl->_psi->_bDirty ) {
      if ( ! SettingInfo_saveToFile( ptpl->_psi ) ) {
        pThis->_base.zErrMsg = sqlite3_mprintf ( "failed to save value" );
        return SQLITE_ERROR;
      }
    }
    ptpl->_psi->_bNew = false;
    ptpl->_psi->_bDirty = false;
    fsl_buffer_reset ( &ptpl->_psi->_strOrigValue );
  }

  /*... and delete 'deleted ones', unless they got re-inserted*/
  for ( nIdx = 0; nIdx < (int) pThis->_deletedSettings.used; ++nIdx ) {
    SettingCollTuple* ptpl = (SettingCollTuple*)(pThis->_deletedSettings.list[nIdx]);
    int nIdxRealized = vtblVerSet_table_find_setting_idx_by_name ( 
        pThis, fsl_buffer_cstr ( &ptpl->_psi->_strName ) );
    if ( nIdxRealized < 0 ) {
      if ( ! SettingInfo_removeFile ( ptpl->_psi ) ) {
        pThis->_base.zErrMsg = sqlite3_mprintf ( "failed to delete value" );
        return SQLITE_ERROR;
      }
    }
  }
  /*clear deleted list*/
  fsl_list_clear ( &pThis->_deletedSettings, fsl_list_visitor_settingtuple_free, NULL );

  return SQLITE_OK;
}


/*end a transaction on a virtual table, discarding changes*/
static int vtblVerSet_Rollback ( sqlite3_vtab* pVTab )
{
  vtblVerSet_table* pThis = DOWNCAST(vtblVerSet_table,_base,pVTab);
  int nIdx;

  /*whiz through 'deleted ones', transferring back or replacing any existing*/
  nIdx = 0;
  while ( nIdx < (int) pThis->_deletedSettings.used ) {
    SettingCollTuple* ptplDeleted = (SettingCollTuple*)(pThis->_deletedSettings.list[nIdx]);
    int nIdxRealized = vtblVerSet_table_find_setting_idx_by_name ( 
        pThis, fsl_buffer_cstr ( &ptplDeleted->_psi->_strName ) );
    if ( nIdxRealized < 0 ) {
      /*transfer back*/
      implTransferSettingListByIdx ( &pThis->_settings, &pThis->_deletedSettings, nIdx );
    }
    else {
      /*replace existing*/
      implTransferReplaceSettingListByIdx ( &pThis->_settings, nIdxRealized,
          &pThis->_deletedSettings, nIdx );
    }
  }
  /*clear deleted list (should be empty anyway from all the transfers)*/
  fsl_list_clear ( &pThis->_deletedSettings, fsl_list_visitor_settingtuple_free, NULL );

  /*whizz through all settings; transfer back data on 'dirty' ones, remove 'new' ones*/
  for ( nIdx = 0; nIdx < (int) pThis->_settings.used; ) {
    SettingCollTuple* ptpl = (SettingCollTuple*)(pThis->_settings.list[nIdx]);
    if ( ptpl->_psi->_bNew ) {
      /*remove*/
      vtblVerSet_table_erase_setting_by_idx ( pThis, nIdx );
    }
    else if ( ptpl->_psi->_bDirty ) {
      /*revert*/
      fsl_buffer_swap ( &ptpl->_psi->_strValue, &ptpl->_psi->_strOrigValue );
      fsl_buffer_reset ( &ptpl->_psi->_strOrigValue );
      ptpl->_psi->_bNew = false;
      ptpl->_psi->_bDirty = false;
      /*advance to next setting since we're keeping this (reverted) one*/
      ++nIdx;
    }
    else {
      /*this one was unaltered*/
      ++nIdx;
    }
  }

  return SQLITE_OK;
}



/*here, we preprocess a regex for our 'extended' escapes (which must be
at the start of the regex; we just have a couple, and set bools to indicate
whether they were detected.  we return a pointer to the 'rest' of the
regex, which can be processed as per usual.*/
static const char* _implPreprocessRegex ( const char* pszRegex,
    bool* pbInsensitive, bool* pbMultivar, bool* pbEmptyOK ) {
  const char* pszRest = pszRegex;
  *pbInsensitive = false; /*init defaults for params we are parsing*/
  *pbMultivar = false;
  while ( '\0' != *pszRest ) {
    /*must start with our special escape sequence*/
    if ( '\\' != pszRest[0] || '$' != pszRest[1] ) {
      break;
    }
    /*must be one of our known instructions*/
    if ( 'i' == pszRest[2] ) {
      *pbInsensitive = true;
    }
    else if ( 'm' == pszRest[2] ) {
      *pbMultivar = true;
    }
    else if ( 'e' == pszRest[2] ) {
      *pbEmptyOK = true;
    }
    else
      break;
    /*consume*/
    pszRest += 3;
  }
  return pszRest;
}


/*context data stored in 'aux data'*/
typedef struct tagMatchOrFailFxnCtx MatchOrFailFxnCtx;
struct tagMatchOrFailFxnCtx
{
  sqlite3_ReCompiled* pRe;  /* Compiled regular expression */
  bool bInsensitive;  /*and rude*/
  bool bMultivar;     /*must we crack it into several items?*/
  bool bEmptyOK;      /*is empty input text considered a match?*/
};

void MatchOrFailFxnCtx_delete ( MatchOrFailFxnCtx* pThis ) {
  sqlite3re_free ( pThis->pRe );
  fsl_free ( pThis );
}

bool MatchOrFailFxnCtx_match ( MatchOrFailFxnCtx* pThis, const char* pszStr ) {
  if ( '\0' == pszStr[0] && pThis->bEmptyOK ) {
    return true;
  }
  else if ( ! pThis->bMultivar ) {
    return sqlite3re_match ( pThis->pRe, (const unsigned char*)pszStr, -1 );
  }
  else {
    /*this is a 'multivar' value, so crack it, and test them all individually*/
    fsl_list lstrValues = fsl_list_empty;
    fsl_size_t nIdx;
    bool bResult;
    /*while it is called a 'glob_list', in fact, it is just a list of strings*/
    if ( 0 != fsl_glob_list_parse ( &lstrValues, pszStr ) ) {
      /*horror*/
      return false;
    }
    /*for each; match.  return logical 'and' over all tests*/
    bResult = true;
    for ( nIdx = 0; bResult && nIdx < lstrValues.used; ++nIdx ) {
      if ( ! sqlite3re_match ( pThis->pRe,
          (const unsigned char*)lstrValues.list[nIdx], -1 ) ) {
        bResult = false;
      }
    }
    fsl_glob_list_clear ( &lstrValues );
    return bResult;
  }
}



/*This is a sqlite3 extension function that either matches a text to a regex
  pattern, or fails (with a SQLITE_CONSTRAINT error.  This is intended to be
  used for value validation for INSERTs or UPDATE, and was created for the
  fossil settings vtable, but it may be of general use, since it doesn't have
  any special bindings to that system.
  Some things of note:  NULL pattern yields NULL result.  Empty pattern (non-
  null, zero length) matches everything.
  This implementation uses the sqlite3 sample regexp engine, but it is simple
  enough that you could easily use another, more powerful, engine, like PCRE.
  argv[0] = text to match
  argv[1] = regex
*/
static void implMatchOrFail ( sqlite3_context* pctx, int argc, sqlite3_value** argv ) {
  MatchOrFailFxnCtx* pCtxMoF;
  const char* zStr;         /* String being searched */
  /*we registered this method explicitly as taking two parameters, so we don't
  need to check that assuption*/

  /*we definitely need the text to test, and do some pre-checks*/
  zStr = (const char*) sqlite3_value_text ( argv[0] );
  if ( NULL == zStr ) {
    sqlite3_result_null ( pctx );
    return;
  }

  /*see if we precomputed this, and compute if needed*/
  pCtxMoF = (MatchOrFailFxnCtx*)sqlite3_get_auxdata ( pctx, 1 ); /*precomputed?*/
  if ( NULL == pCtxMoF ) {  /*no, must compile it now*/
    const char* zErr;         /* Compile error message */
    sqlite3_ReCompiled* pRe;  /* Compiled regular expression */
    const char* zPattern;     /*The regular expression*/
    const char* pszProcessedPattern;  /*the pattern after custom escapes*/
    bool bInsensitive;  /*and rude*/
    bool bMultivar;     /*must we crack it into several items?*/
    bool bEmptyOK;      /*is the empty string OK?*/

    zPattern = (const char*) sqlite3_value_text ( argv[1] );
    /*null pattern == null return*/
    if ( NULL == zPattern ) {
      sqlite3_result_null ( pctx );
      return;
    }
    /*empty pattern == never match*/
    if ( '\0' == zPattern[0] ) {
      sqlite3_result_error_code ( pctx, SQLITE_CONSTRAINT );
      return;
    }

    /*parse off our special metachars, if any*/
    pszProcessedPattern = _implPreprocessRegex ( zPattern,
        &bInsensitive, &bMultivar, &bEmptyOK );

    /*compile regex machine*/
    zErr = sqlite3re_compile ( &pRe, pszProcessedPattern, bInsensitive );
    if ( NULL != zErr ) {
      sqlite3re_free ( pRe );
      sqlite3_result_error ( pctx, zErr, -1 );
      return;
    }
    if ( NULL == pRe ) {
      sqlite3_result_error_nomem ( pctx );
      return;
    }

    /*remember...*/
    pCtxMoF = (MatchOrFailFxnCtx*) fsl_malloc ( sizeof(MatchOrFailFxnCtx) );
    if( !pCtxMoF ){
      sqlite3re_free( pRe );
      sqlite3_result_error_nomem ( pctx );
      return;
    }
    pCtxMoF->pRe = pRe;
    pCtxMoF->bInsensitive = bInsensitive;
    pCtxMoF->bMultivar = bMultivar;
    pCtxMoF->bEmptyOK = bEmptyOK;
    sqlite3_set_auxdata ( pctx, 1, pCtxMoF, (void(*)(void*))MatchOrFailFxnCtx_delete );
  }
  
  /*OK, now, finally we can test*/
  if ( MatchOrFailFxnCtx_match ( pCtxMoF, zStr ) ) {
    sqlite3_result_text ( pctx, zStr, -1, SQLITE_TRANSIENT );
  } else {
    sqlite3_result_error_code ( pctx, SQLITE_CONSTRAINT );
  }

}



/*'overloading' functions*/
static int vtblVerSet_FindFunction ( sqlite3_vtab* pVtab, int nArg, const char* zName,
    void (**pxFunc) ( sqlite3_context*, int, sqlite3_value** ), void** ppArg ) {
  return 0;   /*do not override*/
}


static int vtblVerSet_Rename ( sqlite3_vtab* pVtab, const char* zNew ) {
  return SQLITE_OK;   /*we don't care about being renamed*/
}


static const sqlite3_module sg_pvtblVersionedSettings = {
  2,                          /*iVersion -- version of this structure, so sqlite doesn't access past end*/
  vtblVerSet_Create,          /*xCreate -- 'create virtual table' called; make a sqlite3_vtab*/
  vtblVerSet_Connect,         /*xConnect -- connect to an existing vtable; relevant for dealing with idempotently instantiated state, otherwise the same as xCreate*/
  vtblVerSet_BestIndex,       /*xBestIndex -- prepare for query, indicate what filtering we can do efficiently ourselves*/
  vtblVerSet_Disconnect,      /*xDisconnect -- drop virtual table, but not the last one*/
  vtblVerSet_Destroy,         /*xDestroy -- drop virtual table; the last one, so global cleanup can be done*/
  vtblVerSet_cursor_Open,     /*xOpen -- open a cursor*/
  vtblVerSet_cursor_Close,    /*xClose -- close a cursor*/
  vtblVerSet_cursor_Filter,   /*xFilter -- configure scan constraints*/
  vtblVerSet_cursor_Next,     /*xNext -- advance a cursor*/
  vtblVerSet_cursor_Eof,      /*xEof -- check for end of scan*/
  vtblVerSet_cursor_Column,   /*xColumn -- read data*/
  vtblVerSet_cursor_Rowid,    /*xRowid -- read data*/
  /*updateable vtables implement some of these*/
  vtblVerSet_Update,          /*xUpdate -- insert/update/delete by rowid*/
  vtblVerSet_Begin,           /*xBegin -- begin a transaction*/
  vtblVerSet_Sync,            /*xSync -- prepare for 2-phase commit*/
  vtblVerSet_Commit,          /*xCommit -- commit a transaction*/
  vtblVerSet_Rollback,        /*xRollback -- revert a transaction*/
  vtblVerSet_FindFunction,    /*xFindFunction -- find a private implementation of a function*/
  vtblVerSet_Rename,          /*xRename -- table is being renamed*/
  /*v2 methods; 3.7.7; for nested transactions*/
  NULL,                       /*int (*xSavepoint)(sqlite3_vtab *pVTab, int);*/
  NULL,                       /*int (*xRelease)(sqlite3_vtab *pVTab, int);*/
  NULL,                       /*int (*xRollbackTo)(sqlite3_vtab *pVTab, int);*/
};




/*========================================================================*/
/*register*/




int sqlite3_vtbl_fossilsettings_init (
    sqlite3* db,
    char** pzErrMsg,
    const struct sqlite3_api_routines* pApi
    ) {
  /* int nRet; */
#ifndef SQLITE_CORE
  SQLITE_EXTENSION_INIT2(pApi)    /*sets up the 'vtable' to the host exe sqlite3 impl*/
#else
#endif

#ifndef SQLITE_OMIT_VIRTUALTABLE
  /* nRet =  */sqlite3_create_module_v2 ( db, "SETTINGS_METADATA", &sg_pvtblSettingsMetadata, NULL, NULL );
  /* nRet =  */sqlite3_create_module_v2 ( db, "VERSIONED_SETTINGS", &sg_pvtblVersionedSettings, NULL, NULL );
#endif

  /* nRet =  */sqlite3_create_function ( db, "MATCH_OR_FAIL", 2, SQLITE_UTF8, 0, implMatchOrFail, 0, 0 );

  return SQLITE_OK;
}


#undef COUNTOF
#undef DOWNCAST
#undef bool
#undef false
#undef true
