diff options
Diffstat (limited to 'ext/misc/unionvtab.c')
-rw-r--r-- | ext/misc/unionvtab.c | 1382 |
1 files changed, 1382 insertions, 0 deletions
diff --git a/ext/misc/unionvtab.c b/ext/misc/unionvtab.c new file mode 100644 index 0000000..6ac7ca6 --- /dev/null +++ b/ext/misc/unionvtab.c @@ -0,0 +1,1382 @@ +/* +** 2017 July 15 +** +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +************************************************************************* +** +** This file contains the implementation of the "unionvtab" and "swarmvtab" +** virtual tables. These modules provide read-only access to multiple tables, +** possibly in multiple database files, via a single database object. +** The source tables must have the following characteristics: +** +** * They must all be rowid tables (not VIRTUAL or WITHOUT ROWID +** tables or views). +** +** * Each table must have the same set of columns, declared in +** the same order and with the same declared types. +** +** * The tables must not feature a user-defined column named "_rowid_". +** +** * Each table must contain a distinct range of rowid values. +** +** The difference between the two virtual table modules is that for +** "unionvtab", all source tables must be located in the main database or +** in databases ATTACHed to the main database by the user. For "swarmvtab", +** the tables may be located in any database file on disk. The "swarmvtab" +** implementation takes care of opening and closing database files +** automatically. +** +** UNIONVTAB +** +** A "unionvtab" virtual table is created as follows: +** +** CREATE VIRTUAL TABLE <name> USING unionvtab(<sql-statement>); +** +** The implementation evalutes <sql statement> whenever a unionvtab virtual +** table is created or opened. It should return one row for each source +** database table. The four columns required of each row are: +** +** 1. The name of the database containing the table ("main" or "temp" or +** the name of an attached database). Or NULL to indicate that all +** databases should be searched for the table in the usual fashion. +** +** 2. The name of the database table. +** +** 3. The smallest rowid in the range of rowids that may be stored in the +** database table (an integer). +** +** 4. The largest rowid in the range of rowids that may be stored in the +** database table (an integer). +** +** SWARMVTAB +** +** LEGACY SYNTAX: +** +** A "swarmvtab" virtual table is created similarly to a unionvtab table: +** +** CREATE VIRTUAL TABLE <name> +** USING swarmvtab(<sql-statement>, <callback>); +** +** The difference is that for a swarmvtab table, the first column returned +** by the <sql statement> must return a path or URI that can be used to open +** the database file containing the source table. The <callback> option +** is optional. If included, it is the name of an application-defined +** SQL function that is invoked with the URI of the file, if the file +** does not already exist on disk when required by swarmvtab. +** +** NEW SYNTAX: +** +** Using the new syntax, a swarmvtab table is created with: +** +** CREATE VIRTUAL TABLE <name> USING swarmvtab( +** <sql-statement> [, <options>] +** ); +** +** where valid <options> are: +** +** missing=<udf-function-name> +** openclose=<udf-function-name> +** maxopen=<integer> +** <sql-parameter>=<text-value> +** +** The <sql-statement> must return the same 4 columns as for a swarmvtab +** table in legacy mode. However, it may also return a 5th column - the +** "context" column. The text value returned in this column is not used +** at all by the swarmvtab implementation, except that it is passed as +** an additional argument to the two UDF functions that may be invoked +** (see below). +** +** The "missing" option, if present, specifies the name of an SQL UDF +** function to be invoked if a database file is not already present on +** disk when required by swarmvtab. If the <sql-statement> did not provide +** a context column, it is invoked as: +** +** SELECT <missing-udf>(<database filename/uri>); +** +** Or, if there was a context column: +** +** SELECT <missing-udf>(<database filename/uri>, <context>); +** +** The "openclose" option may also specify a UDF function. This function +** is invoked right before swarmvtab opens a database, and right after +** it closes one. The first argument - or first two arguments, if +** <sql-statement> supplied the context column - is the same as for +** the "missing" UDF. Following this, the UDF is passed integer value +** 0 before a db is opened, and 1 right after it is closed. If both +** a missing and openclose UDF is supplied, the application should expect +** the following sequence of calls (for a single database): +** +** SELECT <openclose-udf>(<db filename>, <context>, 0); +** if( db not already on disk ){ +** SELECT <missing-udf>(<db filename>, <context>); +** } +** ... swarmvtab uses database ... +** SELECT <openclose-udf>(<db filename>, <context>, 1); +** +** The "maxopen" option is used to configure the maximum number of +** database files swarmvtab will hold open simultaneously (default 9). +** +** If an option name begins with a ":" character, then it is assumed +** to be an SQL parameter. In this case, the specified text value is +** bound to the same variable of the <sql-statement> before it is +** executed. It is an error of the named SQL parameter does not exist. +** For example: +** +** CREATE VIRTUAL TABLE swarm USING swarmvtab( +** 'SELECT :path || localfile, tbl, min, max FROM swarmdir', +** :path='/home/user/databases/' +** missing='missing_func' +** ); +*/ + +#include "sqlite3ext.h" +SQLITE_EXTENSION_INIT1 +#include <assert.h> +#include <string.h> +#include <stdlib.h> + +#ifndef SQLITE_OMIT_VIRTUALTABLE + +/* +** Largest and smallest possible 64-bit signed integers. These macros +** copied from sqliteInt.h. +*/ +#ifndef LARGEST_INT64 +# define LARGEST_INT64 (0xffffffff|(((sqlite3_int64)0x7fffffff)<<32)) +#endif +#ifndef SMALLEST_INT64 +# define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64) +#endif + +/* +** The following is also copied from sqliteInt.h. To facilitate coverage +** testing. +*/ +#ifndef ALWAYS +# if defined(SQLITE_COVERAGE_TEST) || defined(SQLITE_MUTATION_TEST) +# define ALWAYS(X) (1) +# define NEVER(X) (0) +# elif !defined(NDEBUG) +# define ALWAYS(X) ((X)?1:(assert(0),0)) +# define NEVER(X) ((X)?(assert(0),1):0) +# else +# define ALWAYS(X) (X) +# define NEVER(X) (X) +# endif +#endif + +/* +** The swarmvtab module attempts to keep the number of open database files +** at or below this limit. This may not be possible if there are too many +** simultaneous queries. +*/ +#define SWARMVTAB_MAX_OPEN 9 + +typedef struct UnionCsr UnionCsr; +typedef struct UnionTab UnionTab; +typedef struct UnionSrc UnionSrc; + +/* +** Each source table (row returned by the initialization query) is +** represented by an instance of the following structure stored in the +** UnionTab.aSrc[] array. +*/ +struct UnionSrc { + char *zDb; /* Database containing source table */ + char *zTab; /* Source table name */ + sqlite3_int64 iMin; /* Minimum rowid */ + sqlite3_int64 iMax; /* Maximum rowid */ + + /* Fields used by swarmvtab only */ + char *zFile; /* Database file containing table zTab */ + char *zContext; /* Context string, if any */ + int nUser; /* Current number of users */ + sqlite3 *db; /* Database handle */ + UnionSrc *pNextClosable; /* Next in list of closable sources */ +}; + +/* +** Virtual table type for union vtab. +*/ +struct UnionTab { + sqlite3_vtab base; /* Base class - must be first */ + sqlite3 *db; /* Database handle */ + int bSwarm; /* 1 for "swarmvtab", 0 for "unionvtab" */ + int iPK; /* INTEGER PRIMARY KEY column, or -1 */ + int nSrc; /* Number of elements in the aSrc[] array */ + UnionSrc *aSrc; /* Array of source tables, sorted by rowid */ + + /* Used by swarmvtab only */ + int bHasContext; /* Has context strings */ + char *zSourceStr; /* Expected unionSourceToStr() value */ + sqlite3_stmt *pNotFound; /* UDF to invoke if file not found on open */ + sqlite3_stmt *pOpenClose; /* UDF to invoke on open and close */ + + UnionSrc *pClosable; /* First in list of closable sources */ + int nOpen; /* Current number of open sources */ + int nMaxOpen; /* Maximum number of open sources */ +}; + +/* +** Virtual table cursor type for union vtab. +*/ +struct UnionCsr { + sqlite3_vtab_cursor base; /* Base class - must be first */ + sqlite3_stmt *pStmt; /* SQL statement to run */ + + /* Used by swarmvtab only */ + sqlite3_int64 iMaxRowid; /* Last rowid to visit */ + int iTab; /* Index of table read by pStmt */ +}; + +/* +** Given UnionTab table pTab and UnionSrc object pSrc, return the database +** handle that should be used to access the table identified by pSrc. This +** is the main db handle for "unionvtab" tables, or the source-specific +** handle for "swarmvtab". +*/ +#define unionGetDb(pTab, pSrc) ((pTab)->bSwarm ? (pSrc)->db : (pTab)->db) + +/* +** If *pRc is other than SQLITE_OK when this function is called, it +** always returns NULL. Otherwise, it attempts to allocate and return +** a pointer to nByte bytes of zeroed memory. If the memory allocation +** is attempted but fails, NULL is returned and *pRc is set to +** SQLITE_NOMEM. +*/ +static void *unionMalloc(int *pRc, sqlite3_int64 nByte){ + void *pRet; + assert( nByte>0 ); + if( *pRc==SQLITE_OK ){ + pRet = sqlite3_malloc64(nByte); + if( pRet ){ + memset(pRet, 0, (size_t)nByte); + }else{ + *pRc = SQLITE_NOMEM; + } + }else{ + pRet = 0; + } + return pRet; +} + +/* +** If *pRc is other than SQLITE_OK when this function is called, it +** always returns NULL. Otherwise, it attempts to allocate and return +** a copy of the nul-terminated string passed as the second argument. +** If the allocation is attempted but fails, NULL is returned and *pRc is +** set to SQLITE_NOMEM. +*/ +static char *unionStrdup(int *pRc, const char *zIn){ + char *zRet = 0; + if( zIn ){ + sqlite3_int64 nByte = strlen(zIn) + 1; + zRet = unionMalloc(pRc, nByte); + if( zRet ){ + memcpy(zRet, zIn, (size_t)nByte); + } + } + return zRet; +} + +/* +** If the first character of the string passed as the only argument to this +** function is one of the 4 that may be used as an open quote character +** in SQL, this function assumes that the input is a well-formed quoted SQL +** string. In this case the string is dequoted in place. +** +** If the first character of the input is not an open quote, then this +** function is a no-op. +*/ +static void unionDequote(char *z){ + if( z ){ + char q = z[0]; + + /* Set stack variable q to the close-quote character */ + if( q=='[' || q=='\'' || q=='"' || q=='`' ){ + int iIn = 1; + int iOut = 0; + if( q=='[' ) q = ']'; + while( ALWAYS(z[iIn]) ){ + if( z[iIn]==q ){ + if( z[iIn+1]!=q ){ + /* Character iIn was the close quote. */ + iIn++; + break; + }else{ + /* Character iIn and iIn+1 form an escaped quote character. Skip + ** the input cursor past both and copy a single quote character + ** to the output buffer. */ + iIn += 2; + z[iOut++] = q; + } + }else{ + z[iOut++] = z[iIn++]; + } + } + z[iOut] = '\0'; + } + } +} + +/* +** This function is a no-op if *pRc is set to other than SQLITE_OK when it +** is called. NULL is returned in this case. +** +** Otherwise, the SQL statement passed as the third argument is prepared +** against the database handle passed as the second. If the statement is +** successfully prepared, a pointer to the new statement handle is +** returned. It is the responsibility of the caller to eventually free the +** statement by calling sqlite3_finalize(). Alternatively, if statement +** compilation fails, NULL is returned, *pRc is set to an SQLite error +** code and *pzErr may be set to an error message buffer allocated by +** sqlite3_malloc(). +*/ +static sqlite3_stmt *unionPrepare( + int *pRc, /* IN/OUT: Error code */ + sqlite3 *db, /* Database handle */ + const char *zSql, /* SQL statement to prepare */ + char **pzErr /* OUT: Error message */ +){ + sqlite3_stmt *pRet = 0; + assert( pzErr ); + if( *pRc==SQLITE_OK ){ + int rc = sqlite3_prepare_v2(db, zSql, -1, &pRet, 0); + if( rc!=SQLITE_OK ){ + *pzErr = sqlite3_mprintf("sql error: %s", sqlite3_errmsg(db)); + *pRc = rc; + } + } + return pRet; +} + +/* +** Like unionPrepare(), except prepare the results of vprintf(zFmt, ...) +** instead of a constant SQL string. +*/ +static sqlite3_stmt *unionPreparePrintf( + int *pRc, /* IN/OUT: Error code */ + char **pzErr, /* OUT: Error message */ + sqlite3 *db, /* Database handle */ + const char *zFmt, /* printf() format string */ + ... /* Trailing printf args */ +){ + sqlite3_stmt *pRet = 0; + char *zSql; + va_list ap; + va_start(ap, zFmt); + + zSql = sqlite3_vmprintf(zFmt, ap); + if( *pRc==SQLITE_OK ){ + if( zSql==0 ){ + *pRc = SQLITE_NOMEM; + }else{ + pRet = unionPrepare(pRc, db, zSql, pzErr); + } + } + sqlite3_free(zSql); + + va_end(ap); + return pRet; +} + + +/* +** Call sqlite3_reset() on SQL statement pStmt. If *pRc is set to +** SQLITE_OK when this function is called, then it is set to the +** value returned by sqlite3_reset() before this function exits. +** In this case, *pzErr may be set to point to an error message +** buffer allocated by sqlite3_malloc(). +*/ +#if 0 +static void unionReset(int *pRc, sqlite3_stmt *pStmt, char **pzErr){ + int rc = sqlite3_reset(pStmt); + if( *pRc==SQLITE_OK ){ + *pRc = rc; + if( rc ){ + *pzErr = sqlite3_mprintf("%s", sqlite3_errmsg(sqlite3_db_handle(pStmt))); + } + } +} +#endif + +/* +** Call sqlite3_finalize() on SQL statement pStmt. If *pRc is set to +** SQLITE_OK when this function is called, then it is set to the +** value returned by sqlite3_finalize() before this function exits. +*/ +static void unionFinalize(int *pRc, sqlite3_stmt *pStmt, char **pzErr){ + sqlite3 *db = sqlite3_db_handle(pStmt); + int rc = sqlite3_finalize(pStmt); + if( *pRc==SQLITE_OK ){ + *pRc = rc; + if( rc ){ + *pzErr = sqlite3_mprintf("%s", sqlite3_errmsg(db)); + } + } +} + +/* +** If an "openclose" UDF was supplied when this virtual table was created, +** invoke it now. The first argument passed is the name of the database +** file for source pSrc. The second is integer value bClose. +** +** If successful, return SQLITE_OK. Otherwise an SQLite error code. In this +** case if argument pzErr is not NULL, also set (*pzErr) to an English +** language error message. The caller is responsible for eventually freeing +** any error message using sqlite3_free(). +*/ +static int unionInvokeOpenClose( + UnionTab *pTab, + UnionSrc *pSrc, + int bClose, + char **pzErr +){ + int rc = SQLITE_OK; + if( pTab->pOpenClose ){ + sqlite3_bind_text(pTab->pOpenClose, 1, pSrc->zFile, -1, SQLITE_STATIC); + if( pTab->bHasContext ){ + sqlite3_bind_text(pTab->pOpenClose, 2, pSrc->zContext, -1, SQLITE_STATIC); + } + sqlite3_bind_int(pTab->pOpenClose, 2+pTab->bHasContext, bClose); + sqlite3_step(pTab->pOpenClose); + if( SQLITE_OK!=(rc = sqlite3_reset(pTab->pOpenClose)) ){ + if( pzErr ){ + *pzErr = sqlite3_mprintf("%s", sqlite3_errmsg(pTab->db)); + } + } + } + return rc; +} + +/* +** This function is a no-op for unionvtab. For swarmvtab, it attempts to +** close open database files until at most nMax are open. An SQLite error +** code is returned if an error occurs, or SQLITE_OK otherwise. +*/ +static void unionCloseSources(UnionTab *pTab, int nMax){ + while( pTab->pClosable && pTab->nOpen>nMax ){ + UnionSrc *p; + UnionSrc **pp; + for(pp=&pTab->pClosable; (*pp)->pNextClosable; pp=&(*pp)->pNextClosable); + p = *pp; + assert( p->db ); + sqlite3_close(p->db); + p->db = 0; + *pp = 0; + pTab->nOpen--; + unionInvokeOpenClose(pTab, p, 1, 0); + } +} + +/* +** xDisconnect method. +*/ +static int unionDisconnect(sqlite3_vtab *pVtab){ + if( pVtab ){ + UnionTab *pTab = (UnionTab*)pVtab; + int i; + for(i=0; i<pTab->nSrc; i++){ + UnionSrc *pSrc = &pTab->aSrc[i]; + int bHaveSrcDb = (pSrc->db!=0); + sqlite3_close(pSrc->db); + if( bHaveSrcDb ){ + unionInvokeOpenClose(pTab, pSrc, 1, 0); + } + sqlite3_free(pSrc->zDb); + sqlite3_free(pSrc->zTab); + sqlite3_free(pSrc->zFile); + sqlite3_free(pSrc->zContext); + } + sqlite3_finalize(pTab->pNotFound); + sqlite3_finalize(pTab->pOpenClose); + sqlite3_free(pTab->zSourceStr); + sqlite3_free(pTab->aSrc); + sqlite3_free(pTab); + } + return SQLITE_OK; +} + +/* +** Check that the table identified by pSrc is a rowid table. If not, +** return SQLITE_ERROR and set (*pzErr) to point to an English language +** error message. If the table is a rowid table and no error occurs, +** return SQLITE_OK and leave (*pzErr) unmodified. +*/ +static int unionIsIntkeyTable( + sqlite3 *db, /* Database handle */ + UnionSrc *pSrc, /* Source table to test */ + char **pzErr /* OUT: Error message */ +){ + int bPk = 0; + const char *zType = 0; + int rc; + + sqlite3_table_column_metadata( + db, pSrc->zDb, pSrc->zTab, "_rowid_", &zType, 0, 0, &bPk, 0 + ); + rc = sqlite3_errcode(db); + if( rc==SQLITE_ERROR + || (rc==SQLITE_OK && (!bPk || sqlite3_stricmp("integer", zType))) + ){ + rc = SQLITE_ERROR; + *pzErr = sqlite3_mprintf("no such rowid table: %s%s%s", + (pSrc->zDb ? pSrc->zDb : ""), + (pSrc->zDb ? "." : ""), + pSrc->zTab + ); + } + return rc; +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. In this case it returns NULL. +** +** Otherwise, this function checks that the source table passed as the +** second argument (a) exists, (b) is not a view and (c) has a column +** named "_rowid_" of type "integer" that is the primary key. +** If this is not the case, *pRc is set to SQLITE_ERROR and NULL is +** returned. +** +** Finally, if the source table passes the checks above, a nul-terminated +** string describing the column names and types belonging to the source +** table is returned. Tables with the same set of column names and types +** cause this function to return identical strings. Is is the responsibility +** of the caller to free the returned string using sqlite3_free() when +** it is no longer required. +*/ +static char *unionSourceToStr( + int *pRc, /* IN/OUT: Error code */ + UnionTab *pTab, /* Virtual table object */ + UnionSrc *pSrc, /* Source table to test */ + char **pzErr /* OUT: Error message */ +){ + char *zRet = 0; + if( *pRc==SQLITE_OK ){ + sqlite3 *db = unionGetDb(pTab, pSrc); + int rc = unionIsIntkeyTable(db, pSrc, pzErr); + sqlite3_stmt *pStmt = unionPrepare(&rc, db, + "SELECT group_concat(quote(name) || '.' || quote(type)) " + "FROM pragma_table_info(?, ?)", pzErr + ); + if( rc==SQLITE_OK ){ + sqlite3_bind_text(pStmt, 1, pSrc->zTab, -1, SQLITE_STATIC); + sqlite3_bind_text(pStmt, 2, pSrc->zDb, -1, SQLITE_STATIC); + if( SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *z = (const char*)sqlite3_column_text(pStmt, 0); + zRet = unionStrdup(&rc, z); + } + unionFinalize(&rc, pStmt, pzErr); + } + *pRc = rc; + } + + return zRet; +} + +/* +** Check that all configured source tables exist and have the same column +** names and datatypes. If this is not the case, or if some other error +** occurs, return an SQLite error code. In this case *pzErr may be set +** to point to an error message buffer allocated by sqlite3_mprintf(). +** Or, if no problems regarding the source tables are detected and no +** other error occurs, SQLITE_OK is returned. +*/ +static int unionSourceCheck(UnionTab *pTab, char **pzErr){ + int rc = SQLITE_OK; + char *z0 = 0; + int i; + + assert( *pzErr==0 ); + z0 = unionSourceToStr(&rc, pTab, &pTab->aSrc[0], pzErr); + for(i=1; i<pTab->nSrc; i++){ + char *z = unionSourceToStr(&rc, pTab, &pTab->aSrc[i], pzErr); + if( rc==SQLITE_OK && sqlite3_stricmp(z, z0) ){ + *pzErr = sqlite3_mprintf("source table schema mismatch"); + rc = SQLITE_ERROR; + } + sqlite3_free(z); + } + sqlite3_free(z0); + + return rc; +} + +/* +** Try to open the swarmvtab database. If initially unable, invoke the +** not-found callback UDF and then try again. +*/ +static int unionOpenDatabaseInner(UnionTab *pTab, UnionSrc *pSrc, char **pzErr){ + static const int openFlags = SQLITE_OPEN_READONLY | SQLITE_OPEN_URI; + int rc; + + rc = unionInvokeOpenClose(pTab, pSrc, 0, pzErr); + if( rc!=SQLITE_OK ) return rc; + + rc = sqlite3_open_v2(pSrc->zFile, &pSrc->db, openFlags, 0); + if( rc==SQLITE_OK ) return rc; + if( pTab->pNotFound ){ + sqlite3_close(pSrc->db); + pSrc->db = 0; + sqlite3_bind_text(pTab->pNotFound, 1, pSrc->zFile, -1, SQLITE_STATIC); + if( pTab->bHasContext ){ + sqlite3_bind_text(pTab->pNotFound, 2, pSrc->zContext, -1, SQLITE_STATIC); + } + sqlite3_step(pTab->pNotFound); + if( SQLITE_OK!=(rc = sqlite3_reset(pTab->pNotFound)) ){ + *pzErr = sqlite3_mprintf("%s", sqlite3_errmsg(pTab->db)); + return rc; + } + rc = sqlite3_open_v2(pSrc->zFile, &pSrc->db, openFlags, 0); + } + if( rc!=SQLITE_OK ){ + *pzErr = sqlite3_mprintf("%s", sqlite3_errmsg(pSrc->db)); + } + return rc; +} + +/* +** This function may only be called for swarmvtab tables. The results of +** calling it on a unionvtab table are undefined. +** +** For a swarmvtab table, this function ensures that source database iSrc +** is open. If the database is opened successfully and the schema is as +** expected, or if it is already open when this function is called, SQLITE_OK +** is returned. +** +** Alternatively If an error occurs while opening the databases, or if the +** database schema is unsuitable, an SQLite error code is returned and (*pzErr) +** may be set to point to an English language error message. In this case it is +** the responsibility of the caller to eventually free the error message buffer +** using sqlite3_free(). +*/ +static int unionOpenDatabase(UnionTab *pTab, int iSrc, char **pzErr){ + int rc = SQLITE_OK; + UnionSrc *pSrc = &pTab->aSrc[iSrc]; + + assert( pTab->bSwarm && iSrc<pTab->nSrc ); + if( pSrc->db==0 ){ + unionCloseSources(pTab, pTab->nMaxOpen-1); + rc = unionOpenDatabaseInner(pTab, pSrc, pzErr); + if( rc==SQLITE_OK ){ + char *z = unionSourceToStr(&rc, pTab, pSrc, pzErr); + if( rc==SQLITE_OK ){ + if( pTab->zSourceStr==0 ){ + pTab->zSourceStr = z; + }else{ + if( sqlite3_stricmp(z, pTab->zSourceStr) ){ + *pzErr = sqlite3_mprintf("source table schema mismatch"); + rc = SQLITE_ERROR; + } + sqlite3_free(z); + } + } + } + + if( rc==SQLITE_OK ){ + pSrc->pNextClosable = pTab->pClosable; + pTab->pClosable = pSrc; + pTab->nOpen++; + }else{ + sqlite3_close(pSrc->db); + pSrc->db = 0; + unionInvokeOpenClose(pTab, pSrc, 1, 0); + } + } + + return rc; +} + + +/* +** This function is a no-op for unionvtab tables. For swarmvtab, increment +** the reference count for source table iTab. If the reference count was +** zero before it was incremented, also remove the source from the closable +** list. +*/ +static void unionIncrRefcount(UnionTab *pTab, int iTab){ + if( pTab->bSwarm ){ + UnionSrc *pSrc = &pTab->aSrc[iTab]; + assert( pSrc->nUser>=0 && pSrc->db ); + if( pSrc->nUser==0 ){ + UnionSrc **pp; + for(pp=&pTab->pClosable; *pp!=pSrc; pp=&(*pp)->pNextClosable); + *pp = pSrc->pNextClosable; + pSrc->pNextClosable = 0; + } + pSrc->nUser++; + } +} + +/* +** Finalize the SQL statement pCsr->pStmt and return the result. +** +** If this is a swarmvtab table (not unionvtab) and pCsr->pStmt was not +** NULL when this function was called, also decrement the reference +** count on the associated source table. If this means the source tables +** refcount is now zero, add it to the closable list. +*/ +static int unionFinalizeCsrStmt(UnionCsr *pCsr){ + int rc = SQLITE_OK; + if( pCsr->pStmt ){ + UnionTab *pTab = (UnionTab*)pCsr->base.pVtab; + UnionSrc *pSrc = &pTab->aSrc[pCsr->iTab]; + rc = sqlite3_finalize(pCsr->pStmt); + pCsr->pStmt = 0; + if( pTab->bSwarm ){ + pSrc->nUser--; + assert( pSrc->nUser>=0 ); + if( pSrc->nUser==0 ){ + pSrc->pNextClosable = pTab->pClosable; + pTab->pClosable = pSrc; + } + unionCloseSources(pTab, pTab->nMaxOpen); + } + } + return rc; +} + +/* +** Return true if the argument is a space, tab, CR or LF character. +*/ +static int union_isspace(char c){ + return (c==' ' || c=='\n' || c=='\r' || c=='\t'); +} + +/* +** Return true if the argument is an alphanumeric character in the +** ASCII range. +*/ +static int union_isidchar(char c){ + return ((c>='a' && c<='z') || (c>='A' && c<'Z') || (c>='0' && c<='9')); +} + +/* +** This function is called to handle all arguments following the first +** (the SQL statement) passed to a swarmvtab (not unionvtab) CREATE +** VIRTUAL TABLE statement. It may bind parameters to the SQL statement +** or configure members of the UnionTab object passed as the second +** argument. +** +** Refer to header comments at the top of this file for a description +** of the arguments parsed. +** +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, if an error occurs, *pRc is set to an SQLite error +** code. In this case *pzErr may be set to point to a buffer containing +** an English language error message. It is the responsibility of the +** caller to eventually free the buffer using sqlite3_free(). +*/ +static void unionConfigureVtab( + int *pRc, /* IN/OUT: Error code */ + UnionTab *pTab, /* Table to configure */ + sqlite3_stmt *pStmt, /* SQL statement to find sources */ + int nArg, /* Number of entries in azArg[] array */ + const char * const *azArg, /* Array of arguments to consider */ + char **pzErr /* OUT: Error message */ +){ + int rc = *pRc; + int i; + if( rc==SQLITE_OK ){ + pTab->bHasContext = (sqlite3_column_count(pStmt)>4); + } + for(i=0; rc==SQLITE_OK && i<nArg; i++){ + char *zArg = unionStrdup(&rc, azArg[i]); + if( zArg ){ + int nOpt = 0; /* Size of option name in bytes */ + char *zOpt; /* Pointer to option name */ + char *zVal; /* Pointer to value */ + + unionDequote(zArg); + zOpt = zArg; + while( union_isspace(*zOpt) ) zOpt++; + zVal = zOpt; + if( *zVal==':' ) zVal++; + while( union_isidchar(*zVal) ) zVal++; + nOpt = (int)(zVal-zOpt); + + while( union_isspace(*zVal) ) zVal++; + if( *zVal=='=' ){ + zOpt[nOpt] = '\0'; + zVal++; + while( union_isspace(*zVal) ) zVal++; + zVal = unionStrdup(&rc, zVal); + if( zVal ){ + unionDequote(zVal); + if( zOpt[0]==':' ){ + /* A value to bind to the SQL statement */ + int iParam = sqlite3_bind_parameter_index(pStmt, zOpt); + if( iParam==0 ){ + *pzErr = sqlite3_mprintf( + "swarmvtab: no such SQL parameter: %s", zOpt + ); + rc = SQLITE_ERROR; + }else{ + rc = sqlite3_bind_text(pStmt, iParam, zVal, -1, SQLITE_TRANSIENT); + } + }else if( nOpt==7 && 0==sqlite3_strnicmp(zOpt, "maxopen", 7) ){ + pTab->nMaxOpen = atoi(zVal); + if( pTab->nMaxOpen<=0 ){ + *pzErr = sqlite3_mprintf("swarmvtab: illegal maxopen value"); + rc = SQLITE_ERROR; + } + }else if( nOpt==7 && 0==sqlite3_strnicmp(zOpt, "missing", 7) ){ + if( pTab->pNotFound ){ + *pzErr = sqlite3_mprintf( + "swarmvtab: duplicate \"missing\" option"); + rc = SQLITE_ERROR; + }else{ + pTab->pNotFound = unionPreparePrintf(&rc, pzErr, pTab->db, + "SELECT \"%w\"(?%s)", zVal, pTab->bHasContext ? ",?" : "" + ); + } + }else if( nOpt==9 && 0==sqlite3_strnicmp(zOpt, "openclose", 9) ){ + if( pTab->pOpenClose ){ + *pzErr = sqlite3_mprintf( + "swarmvtab: duplicate \"openclose\" option"); + rc = SQLITE_ERROR; + }else{ + pTab->pOpenClose = unionPreparePrintf(&rc, pzErr, pTab->db, + "SELECT \"%w\"(?,?%s)", zVal, pTab->bHasContext ? ",?" : "" + ); + } + }else{ + *pzErr = sqlite3_mprintf("swarmvtab: unrecognized option: %s",zOpt); + rc = SQLITE_ERROR; + } + sqlite3_free(zVal); + } + }else{ + if( i==0 && nArg==1 ){ + pTab->pNotFound = unionPreparePrintf(&rc, pzErr, pTab->db, + "SELECT \"%w\"(?)", zArg + ); + }else{ + *pzErr = sqlite3_mprintf( "swarmvtab: parse error: %s", azArg[i]); + rc = SQLITE_ERROR; + } + } + sqlite3_free(zArg); + } + } + *pRc = rc; +} + +/* +** xConnect/xCreate method. +** +** The argv[] array contains the following: +** +** argv[0] -> module name ("unionvtab" or "swarmvtab") +** argv[1] -> database name +** argv[2] -> table name +** argv[3] -> SQL statement +** argv[4] -> not-found callback UDF name +*/ +static int unionConnect( + sqlite3 *db, + void *pAux, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + UnionTab *pTab = 0; + int rc = SQLITE_OK; + int bSwarm = (pAux==0 ? 0 : 1); + const char *zVtab = (bSwarm ? "swarmvtab" : "unionvtab"); + + if( sqlite3_stricmp("temp", argv[1]) ){ + /* unionvtab tables may only be created in the temp schema */ + *pzErr = sqlite3_mprintf("%s tables must be created in TEMP schema", zVtab); + rc = SQLITE_ERROR; + }else if( argc<4 || (argc>4 && bSwarm==0) ){ + *pzErr = sqlite3_mprintf("wrong number of arguments for %s", zVtab); + rc = SQLITE_ERROR; + }else{ + int nAlloc = 0; /* Allocated size of pTab->aSrc[] */ + sqlite3_stmt *pStmt = 0; /* Argument statement */ + char *zArg = unionStrdup(&rc, argv[3]); /* Copy of argument to CVT */ + + /* Prepare the SQL statement. Instead of executing it directly, sort + ** the results by the "minimum rowid" field. This makes it easier to + ** check that there are no rowid range overlaps between source tables + ** and that the UnionTab.aSrc[] array is always sorted by rowid. */ + unionDequote(zArg); + pStmt = unionPreparePrintf(&rc, pzErr, db, + "SELECT * FROM (%z) ORDER BY 3", zArg + ); + + /* Allocate the UnionTab structure */ + pTab = unionMalloc(&rc, sizeof(UnionTab)); + if( pTab ){ + assert( rc==SQLITE_OK ); + pTab->db = db; + pTab->bSwarm = bSwarm; + pTab->nMaxOpen = SWARMVTAB_MAX_OPEN; + } + + /* Parse other CVT arguments, if any */ + if( bSwarm ){ + unionConfigureVtab(&rc, pTab, pStmt, argc-4, &argv[4], pzErr); + } + + /* Iterate through the rows returned by the SQL statement specified + ** as an argument to the CREATE VIRTUAL TABLE statement. */ + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *zDb = (const char*)sqlite3_column_text(pStmt, 0); + const char *zTab = (const char*)sqlite3_column_text(pStmt, 1); + sqlite3_int64 iMin = sqlite3_column_int64(pStmt, 2); + sqlite3_int64 iMax = sqlite3_column_int64(pStmt, 3); + UnionSrc *pSrc; + + /* Grow the pTab->aSrc[] array if required. */ + if( nAlloc<=pTab->nSrc ){ + int nNew = nAlloc ? nAlloc*2 : 8; + UnionSrc *aNew = (UnionSrc*)sqlite3_realloc64( + pTab->aSrc, nNew*sizeof(UnionSrc) + ); + if( aNew==0 ){ + rc = SQLITE_NOMEM; + break; + }else{ + memset(&aNew[pTab->nSrc], 0, (nNew-pTab->nSrc)*sizeof(UnionSrc)); + pTab->aSrc = aNew; + nAlloc = nNew; + } + } + + /* Check for problems with the specified range of rowids */ + if( iMax<iMin || (pTab->nSrc>0 && iMin<=pTab->aSrc[pTab->nSrc-1].iMax) ){ + *pzErr = sqlite3_mprintf("rowid range mismatch error"); + rc = SQLITE_ERROR; + } + + if( rc==SQLITE_OK ){ + pSrc = &pTab->aSrc[pTab->nSrc++]; + pSrc->zTab = unionStrdup(&rc, zTab); + pSrc->iMin = iMin; + pSrc->iMax = iMax; + if( bSwarm ){ + pSrc->zFile = unionStrdup(&rc, zDb); + }else{ + pSrc->zDb = unionStrdup(&rc, zDb); + } + if( pTab->bHasContext ){ + const char *zContext = (const char*)sqlite3_column_text(pStmt, 4); + pSrc->zContext = unionStrdup(&rc, zContext); + } + } + } + unionFinalize(&rc, pStmt, pzErr); + pStmt = 0; + + /* It is an error if the SELECT statement returned zero rows. If only + ** because there is no way to determine the schema of the virtual + ** table in this case. */ + if( rc==SQLITE_OK && pTab->nSrc==0 ){ + *pzErr = sqlite3_mprintf("no source tables configured"); + rc = SQLITE_ERROR; + } + + /* For unionvtab, verify that all source tables exist and have + ** compatible schemas. For swarmvtab, attach the first database and + ** check that the first table is a rowid table only. */ + if( rc==SQLITE_OK ){ + if( bSwarm ){ + rc = unionOpenDatabase(pTab, 0, pzErr); + }else{ + rc = unionSourceCheck(pTab, pzErr); + } + } + + /* Compose a CREATE TABLE statement and pass it to declare_vtab() */ + if( rc==SQLITE_OK ){ + UnionSrc *pSrc = &pTab->aSrc[0]; + sqlite3 *tdb = unionGetDb(pTab, pSrc); + pStmt = unionPreparePrintf(&rc, pzErr, tdb, "SELECT " + "'CREATE TABLE xyz('" + " || group_concat(quote(name) || ' ' || type, ', ')" + " || ')'," + "max((cid+1) * (type='INTEGER' COLLATE nocase AND pk=1))-1 " + "FROM pragma_table_info(%Q, ?)", + pSrc->zTab, pSrc->zDb + ); + } + if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + const char *zDecl = (const char*)sqlite3_column_text(pStmt, 0); + rc = sqlite3_declare_vtab(db, zDecl); + pTab->iPK = sqlite3_column_int(pStmt, 1); + } + + unionFinalize(&rc, pStmt, pzErr); + } + + if( rc!=SQLITE_OK ){ + unionDisconnect((sqlite3_vtab*)pTab); + pTab = 0; + } + + *ppVtab = (sqlite3_vtab*)pTab; + return rc; +} + +/* +** xOpen +*/ +static int unionOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor){ + UnionCsr *pCsr; + int rc = SQLITE_OK; + (void)p; /* Suppress harmless warning */ + pCsr = (UnionCsr*)unionMalloc(&rc, sizeof(UnionCsr)); + *ppCursor = &pCsr->base; + return rc; +} + +/* +** xClose +*/ +static int unionClose(sqlite3_vtab_cursor *cur){ + UnionCsr *pCsr = (UnionCsr*)cur; + unionFinalizeCsrStmt(pCsr); + sqlite3_free(pCsr); + return SQLITE_OK; +} + +/* +** This function does the work of the xNext() method. Except that, if it +** returns SQLITE_ROW, it should be called again within the same xNext() +** method call. See unionNext() for details. +*/ +static int doUnionNext(UnionCsr *pCsr){ + int rc = SQLITE_OK; + assert( pCsr->pStmt ); + if( sqlite3_step(pCsr->pStmt)!=SQLITE_ROW ){ + UnionTab *pTab = (UnionTab*)pCsr->base.pVtab; + rc = unionFinalizeCsrStmt(pCsr); + if( rc==SQLITE_OK && pTab->bSwarm ){ + pCsr->iTab++; + if( pCsr->iTab<pTab->nSrc ){ + UnionSrc *pSrc = &pTab->aSrc[pCsr->iTab]; + if( pCsr->iMaxRowid>=pSrc->iMin ){ + /* It is necessary to scan the next table. */ + rc = unionOpenDatabase(pTab, pCsr->iTab, &pTab->base.zErrMsg); + pCsr->pStmt = unionPreparePrintf(&rc, &pTab->base.zErrMsg, pSrc->db, + "SELECT rowid, * FROM %Q %s %lld", + pSrc->zTab, + (pSrc->iMax>pCsr->iMaxRowid ? "WHERE _rowid_ <=" : "-- "), + pCsr->iMaxRowid + ); + if( rc==SQLITE_OK ){ + assert( pCsr->pStmt ); + unionIncrRefcount(pTab, pCsr->iTab); + rc = SQLITE_ROW; + } + } + } + } + } + + return rc; +} + +/* +** xNext +*/ +static int unionNext(sqlite3_vtab_cursor *cur){ + int rc; + do { + rc = doUnionNext((UnionCsr*)cur); + }while( rc==SQLITE_ROW ); + return rc; +} + +/* +** xColumn +*/ +static int unionColumn( + sqlite3_vtab_cursor *cur, + sqlite3_context *ctx, + int i +){ + UnionCsr *pCsr = (UnionCsr*)cur; + sqlite3_result_value(ctx, sqlite3_column_value(pCsr->pStmt, i+1)); + return SQLITE_OK; +} + +/* +** xRowid +*/ +static int unionRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ + UnionCsr *pCsr = (UnionCsr*)cur; + *pRowid = sqlite3_column_int64(pCsr->pStmt, 0); + return SQLITE_OK; +} + +/* +** xEof +*/ +static int unionEof(sqlite3_vtab_cursor *cur){ + UnionCsr *pCsr = (UnionCsr*)cur; + return pCsr->pStmt==0; +} + +/* +** xFilter +*/ +static int unionFilter( + sqlite3_vtab_cursor *pVtabCursor, + int idxNum, const char *idxStr, + int argc, sqlite3_value **argv +){ + UnionTab *pTab = (UnionTab*)(pVtabCursor->pVtab); + UnionCsr *pCsr = (UnionCsr*)pVtabCursor; + int rc = SQLITE_OK; + int i; + char *zSql = 0; + int bZero = 0; + + sqlite3_int64 iMin = SMALLEST_INT64; + sqlite3_int64 iMax = LARGEST_INT64; + + assert( idxNum==0 + || idxNum==SQLITE_INDEX_CONSTRAINT_EQ + || idxNum==SQLITE_INDEX_CONSTRAINT_LE + || idxNum==SQLITE_INDEX_CONSTRAINT_GE + || idxNum==SQLITE_INDEX_CONSTRAINT_LT + || idxNum==SQLITE_INDEX_CONSTRAINT_GT + || idxNum==(SQLITE_INDEX_CONSTRAINT_GE|SQLITE_INDEX_CONSTRAINT_LE) + ); + + (void)idxStr; /* Suppress harmless warning */ + + if( idxNum==SQLITE_INDEX_CONSTRAINT_EQ ){ + assert( argc==1 ); + iMin = iMax = sqlite3_value_int64(argv[0]); + }else{ + + if( idxNum & (SQLITE_INDEX_CONSTRAINT_LE|SQLITE_INDEX_CONSTRAINT_LT) ){ + assert( argc>=1 ); + iMax = sqlite3_value_int64(argv[0]); + if( idxNum & SQLITE_INDEX_CONSTRAINT_LT ){ + if( iMax==SMALLEST_INT64 ){ + bZero = 1; + }else{ + iMax--; + } + } + } + + if( idxNum & (SQLITE_INDEX_CONSTRAINT_GE|SQLITE_INDEX_CONSTRAINT_GT) ){ + assert( argc>=1 ); + iMin = sqlite3_value_int64(argv[argc-1]); + if( idxNum & SQLITE_INDEX_CONSTRAINT_GT ){ + if( iMin==LARGEST_INT64 ){ + bZero = 1; + }else{ + iMin++; + } + } + } + } + + unionFinalizeCsrStmt(pCsr); + if( bZero ){ + return SQLITE_OK; + } + + for(i=0; i<pTab->nSrc; i++){ + UnionSrc *pSrc = &pTab->aSrc[i]; + if( iMin>pSrc->iMax || iMax<pSrc->iMin ){ + continue; + } + + zSql = sqlite3_mprintf("%z%sSELECT rowid, * FROM %s%q%s%Q" + , zSql + , (zSql ? " UNION ALL " : "") + , (pSrc->zDb ? "'" : "") + , (pSrc->zDb ? pSrc->zDb : "") + , (pSrc->zDb ? "'." : "") + , pSrc->zTab + ); + if( zSql==0 ){ + rc = SQLITE_NOMEM; + break; + } + + if( iMin==iMax ){ + zSql = sqlite3_mprintf("%z WHERE rowid=%lld", zSql, iMin); + }else{ + const char *zWhere = "WHERE"; + if( iMin!=SMALLEST_INT64 && iMin>pSrc->iMin ){ + zSql = sqlite3_mprintf("%z WHERE rowid>=%lld", zSql, iMin); + zWhere = "AND"; + } + if( iMax!=LARGEST_INT64 && iMax<pSrc->iMax ){ + zSql = sqlite3_mprintf("%z %s rowid<=%lld", zSql, zWhere, iMax); + } + } + + if( pTab->bSwarm ){ + pCsr->iTab = i; + pCsr->iMaxRowid = iMax; + rc = unionOpenDatabase(pTab, i, &pTab->base.zErrMsg); + break; + } + } + + if( zSql==0 ){ + return rc; + }else{ + sqlite3 *db = unionGetDb(pTab, &pTab->aSrc[pCsr->iTab]); + pCsr->pStmt = unionPrepare(&rc, db, zSql, &pTab->base.zErrMsg); + if( pCsr->pStmt ){ + unionIncrRefcount(pTab, pCsr->iTab); + } + sqlite3_free(zSql); + } + if( rc!=SQLITE_OK ) return rc; + return unionNext(pVtabCursor); +} + +/* +** xBestIndex. +** +** This implementation searches for constraints on the rowid field. EQ, +** LE, LT, GE and GT are handled. +** +** If there is an EQ comparison, then idxNum is set to INDEX_CONSTRAINT_EQ. +** In this case the only argument passed to xFilter is the rhs of the == +** operator. +** +** Otherwise, if an LE or LT constraint is found, then the INDEX_CONSTRAINT_LE +** or INDEX_CONSTRAINT_LT (but not both) bit is set in idxNum. The first +** argument to xFilter is the rhs of the <= or < operator. Similarly, if +** an GE or GT constraint is found, then the INDEX_CONSTRAINT_GE or +** INDEX_CONSTRAINT_GT bit is set in idxNum. The rhs of the >= or > operator +** is passed as either the first or second argument to xFilter, depending +** on whether or not there is also a LT|LE constraint. +*/ +static int unionBestIndex( + sqlite3_vtab *tab, + sqlite3_index_info *pIdxInfo +){ + UnionTab *pTab = (UnionTab*)tab; + int iEq = -1; + int iLt = -1; + int iGt = -1; + int i; + + for(i=0; i<pIdxInfo->nConstraint; i++){ + struct sqlite3_index_constraint *p = &pIdxInfo->aConstraint[i]; + if( p->usable && (p->iColumn<0 || p->iColumn==pTab->iPK) ){ + switch( p->op ){ + case SQLITE_INDEX_CONSTRAINT_EQ: + iEq = i; + break; + case SQLITE_INDEX_CONSTRAINT_LE: + case SQLITE_INDEX_CONSTRAINT_LT: + iLt = i; + break; + case SQLITE_INDEX_CONSTRAINT_GE: + case SQLITE_INDEX_CONSTRAINT_GT: + iGt = i; + break; + } + } + } + + if( iEq>=0 ){ + pIdxInfo->estimatedRows = 1; + pIdxInfo->idxFlags = SQLITE_INDEX_SCAN_UNIQUE; + pIdxInfo->estimatedCost = 3.0; + pIdxInfo->idxNum = SQLITE_INDEX_CONSTRAINT_EQ; + pIdxInfo->aConstraintUsage[iEq].argvIndex = 1; + pIdxInfo->aConstraintUsage[iEq].omit = 1; + }else{ + int iCons = 1; + int idxNum = 0; + sqlite3_int64 nRow = 1000000; + if( iLt>=0 ){ + nRow = nRow / 2; + pIdxInfo->aConstraintUsage[iLt].argvIndex = iCons++; + pIdxInfo->aConstraintUsage[iLt].omit = 1; + idxNum |= pIdxInfo->aConstraint[iLt].op; + } + if( iGt>=0 ){ + nRow = nRow / 2; + pIdxInfo->aConstraintUsage[iGt].argvIndex = iCons++; + pIdxInfo->aConstraintUsage[iGt].omit = 1; + idxNum |= pIdxInfo->aConstraint[iGt].op; + } + pIdxInfo->estimatedRows = nRow; + pIdxInfo->estimatedCost = 3.0 * (double)nRow; + pIdxInfo->idxNum = idxNum; + } + + return SQLITE_OK; +} + +/* +** Register the unionvtab virtual table module with database handle db. +*/ +static int createUnionVtab(sqlite3 *db){ + static sqlite3_module unionModule = { + 0, /* iVersion */ + unionConnect, + unionConnect, + unionBestIndex, /* xBestIndex - query planner */ + unionDisconnect, + unionDisconnect, + unionOpen, /* xOpen - open a cursor */ + unionClose, /* xClose - close a cursor */ + unionFilter, /* xFilter - configure scan constraints */ + unionNext, /* xNext - advance a cursor */ + unionEof, /* xEof - check for end of scan */ + unionColumn, /* xColumn - read data */ + unionRowid, /* xRowid - read data */ + 0, /* xUpdate */ + 0, /* xBegin */ + 0, /* xSync */ + 0, /* xCommit */ + 0, /* xRollback */ + 0, /* xFindMethod */ + 0, /* xRename */ + 0, /* xSavepoint */ + 0, /* xRelease */ + 0, /* xRollbackTo */ + 0 /* xShadowName */ + }; + int rc; + + rc = sqlite3_create_module(db, "unionvtab", &unionModule, 0); + if( rc==SQLITE_OK ){ + rc = sqlite3_create_module(db, "swarmvtab", &unionModule, (void*)db); + } + return rc; +} + +#endif /* SQLITE_OMIT_VIRTUALTABLE */ + +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_unionvtab_init( + sqlite3 *db, + char **pzErrMsg, + const sqlite3_api_routines *pApi +){ + int rc = SQLITE_OK; + SQLITE_EXTENSION_INIT2(pApi); + (void)pzErrMsg; /* Suppress harmless warning */ +#ifndef SQLITE_OMIT_VIRTUALTABLE + rc = createUnionVtab(db); +#endif + return rc; +} |