1: /*
2: ** 2010 July 12
3: **
4: ** The author disclaims copyright to this source code. In place of
5: ** a legal notice, here is a blessing:
6: **
7: ** May you do good and not evil.
8: ** May you find forgiveness for yourself and forgive others.
9: ** May you share freely, never taking more than you give.
10: **
11: ******************************************************************************
12: **
13: ** This file contains an implementation of the "dbstat" virtual table.
14: **
15: ** The dbstat virtual table is used to extract low-level formatting
16: ** information from an SQLite database in order to implement the
17: ** "sqlite3_analyzer" utility. See the ../tool/spaceanal.tcl script
18: ** for an example implementation.
19: */
20:
21: #ifndef SQLITE_AMALGAMATION
22: # include "sqliteInt.h"
23: #endif
24:
25: #ifndef SQLITE_OMIT_VIRTUALTABLE
26:
27: /*
28: ** Page paths:
29: **
30: ** The value of the 'path' column describes the path taken from the
31: ** root-node of the b-tree structure to each page. The value of the
32: ** root-node path is '/'.
33: **
34: ** The value of the path for the left-most child page of the root of
35: ** a b-tree is '/000/'. (Btrees store content ordered from left to right
36: ** so the pages to the left have smaller keys than the pages to the right.)
37: ** The next to left-most child of the root page is
38: ** '/001', and so on, each sibling page identified by a 3-digit hex
39: ** value. The children of the 451st left-most sibling have paths such
40: ** as '/1c2/000/, '/1c2/001/' etc.
41: **
42: ** Overflow pages are specified by appending a '+' character and a
43: ** six-digit hexadecimal value to the path to the cell they are linked
44: ** from. For example, the three overflow pages in a chain linked from
45: ** the left-most cell of the 450th child of the root page are identified
46: ** by the paths:
47: **
48: ** '/1c2/000+000000' // First page in overflow chain
49: ** '/1c2/000+000001' // Second page in overflow chain
50: ** '/1c2/000+000002' // Third page in overflow chain
51: **
52: ** If the paths are sorted using the BINARY collation sequence, then
53: ** the overflow pages associated with a cell will appear earlier in the
54: ** sort-order than its child page:
55: **
56: ** '/1c2/000/' // Left-most child of 451st child of root
57: */
58: #define VTAB_SCHEMA \
59: "CREATE TABLE xx( " \
60: " name STRING, /* Name of table or index */" \
61: " path INTEGER, /* Path to page from root */" \
62: " pageno INTEGER, /* Page number */" \
63: " pagetype STRING, /* 'internal', 'leaf' or 'overflow' */" \
64: " ncell INTEGER, /* Cells on page (0 for overflow) */" \
65: " payload INTEGER, /* Bytes of payload on this page */" \
66: " unused INTEGER, /* Bytes of unused space on this page */" \
67: " mx_payload INTEGER, /* Largest payload size of all cells */" \
68: " pgoffset INTEGER, /* Offset of page in file */" \
69: " pgsize INTEGER /* Size of the page */" \
70: ");"
71:
72:
73: typedef struct StatTable StatTable;
74: typedef struct StatCursor StatCursor;
75: typedef struct StatPage StatPage;
76: typedef struct StatCell StatCell;
77:
78: struct StatCell {
79: int nLocal; /* Bytes of local payload */
80: u32 iChildPg; /* Child node (or 0 if this is a leaf) */
81: int nOvfl; /* Entries in aOvfl[] */
82: u32 *aOvfl; /* Array of overflow page numbers */
83: int nLastOvfl; /* Bytes of payload on final overflow page */
84: int iOvfl; /* Iterates through aOvfl[] */
85: };
86:
87: struct StatPage {
88: u32 iPgno;
89: DbPage *pPg;
90: int iCell;
91:
92: char *zPath; /* Path to this page */
93:
94: /* Variables populated by statDecodePage(): */
95: u8 flags; /* Copy of flags byte */
96: int nCell; /* Number of cells on page */
97: int nUnused; /* Number of unused bytes on page */
98: StatCell *aCell; /* Array of parsed cells */
99: u32 iRightChildPg; /* Right-child page number (or 0) */
100: int nMxPayload; /* Largest payload of any cell on this page */
101: };
102:
103: struct StatCursor {
104: sqlite3_vtab_cursor base;
105: sqlite3_stmt *pStmt; /* Iterates through set of root pages */
106: int isEof; /* After pStmt has returned SQLITE_DONE */
107:
108: StatPage aPage[32];
109: int iPage; /* Current entry in aPage[] */
110:
111: /* Values to return. */
112: char *zName; /* Value of 'name' column */
113: char *zPath; /* Value of 'path' column */
114: u32 iPageno; /* Value of 'pageno' column */
115: char *zPagetype; /* Value of 'pagetype' column */
116: int nCell; /* Value of 'ncell' column */
117: int nPayload; /* Value of 'payload' column */
118: int nUnused; /* Value of 'unused' column */
119: int nMxPayload; /* Value of 'mx_payload' column */
120: i64 iOffset; /* Value of 'pgOffset' column */
121: int szPage; /* Value of 'pgSize' column */
122: };
123:
124: struct StatTable {
125: sqlite3_vtab base;
126: sqlite3 *db;
127: };
128:
129: #ifndef get2byte
130: # define get2byte(x) ((x)[0]<<8 | (x)[1])
131: #endif
132:
133: /*
134: ** Connect to or create a statvfs virtual table.
135: */
136: static int statConnect(
137: sqlite3 *db,
138: void *pAux,
139: int argc, const char *const*argv,
140: sqlite3_vtab **ppVtab,
141: char **pzErr
142: ){
143: StatTable *pTab;
144:
145: pTab = (StatTable *)sqlite3_malloc(sizeof(StatTable));
146: memset(pTab, 0, sizeof(StatTable));
147: pTab->db = db;
148:
149: sqlite3_declare_vtab(db, VTAB_SCHEMA);
150: *ppVtab = &pTab->base;
151: return SQLITE_OK;
152: }
153:
154: /*
155: ** Disconnect from or destroy a statvfs virtual table.
156: */
157: static int statDisconnect(sqlite3_vtab *pVtab){
158: sqlite3_free(pVtab);
159: return SQLITE_OK;
160: }
161:
162: /*
163: ** There is no "best-index". This virtual table always does a linear
164: ** scan of the binary VFS log file.
165: */
166: static int statBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo){
167:
168: /* Records are always returned in ascending order of (name, path).
169: ** If this will satisfy the client, set the orderByConsumed flag so that
170: ** SQLite does not do an external sort.
171: */
172: if( ( pIdxInfo->nOrderBy==1
173: && pIdxInfo->aOrderBy[0].iColumn==0
174: && pIdxInfo->aOrderBy[0].desc==0
175: ) ||
176: ( pIdxInfo->nOrderBy==2
177: && pIdxInfo->aOrderBy[0].iColumn==0
178: && pIdxInfo->aOrderBy[0].desc==0
179: && pIdxInfo->aOrderBy[1].iColumn==1
180: && pIdxInfo->aOrderBy[1].desc==0
181: )
182: ){
183: pIdxInfo->orderByConsumed = 1;
184: }
185:
186: pIdxInfo->estimatedCost = 10.0;
187: return SQLITE_OK;
188: }
189:
190: /*
191: ** Open a new statvfs cursor.
192: */
193: static int statOpen(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor){
194: StatTable *pTab = (StatTable *)pVTab;
195: StatCursor *pCsr;
196: int rc;
197:
198: pCsr = (StatCursor *)sqlite3_malloc(sizeof(StatCursor));
199: memset(pCsr, 0, sizeof(StatCursor));
200: pCsr->base.pVtab = pVTab;
201:
202: rc = sqlite3_prepare_v2(pTab->db,
203: "SELECT 'sqlite_master' AS name, 1 AS rootpage, 'table' AS type"
204: " UNION ALL "
205: "SELECT name, rootpage, type FROM sqlite_master WHERE rootpage!=0"
206: " ORDER BY name", -1,
207: &pCsr->pStmt, 0
208: );
209: if( rc!=SQLITE_OK ){
210: sqlite3_free(pCsr);
211: return rc;
212: }
213:
214: *ppCursor = (sqlite3_vtab_cursor *)pCsr;
215: return SQLITE_OK;
216: }
217:
218: static void statClearPage(StatPage *p){
219: int i;
220: for(i=0; i<p->nCell; i++){
221: sqlite3_free(p->aCell[i].aOvfl);
222: }
223: sqlite3PagerUnref(p->pPg);
224: sqlite3_free(p->aCell);
225: sqlite3_free(p->zPath);
226: memset(p, 0, sizeof(StatPage));
227: }
228:
229: static void statResetCsr(StatCursor *pCsr){
230: int i;
231: sqlite3_reset(pCsr->pStmt);
232: for(i=0; i<ArraySize(pCsr->aPage); i++){
233: statClearPage(&pCsr->aPage[i]);
234: }
235: pCsr->iPage = 0;
236: sqlite3_free(pCsr->zPath);
237: pCsr->zPath = 0;
238: }
239:
240: /*
241: ** Close a statvfs cursor.
242: */
243: static int statClose(sqlite3_vtab_cursor *pCursor){
244: StatCursor *pCsr = (StatCursor *)pCursor;
245: statResetCsr(pCsr);
246: sqlite3_finalize(pCsr->pStmt);
247: sqlite3_free(pCsr);
248: return SQLITE_OK;
249: }
250:
251: static void getLocalPayload(
252: int nUsable, /* Usable bytes per page */
253: u8 flags, /* Page flags */
254: int nTotal, /* Total record (payload) size */
255: int *pnLocal /* OUT: Bytes stored locally */
256: ){
257: int nLocal;
258: int nMinLocal;
259: int nMaxLocal;
260:
261: if( flags==0x0D ){ /* Table leaf node */
262: nMinLocal = (nUsable - 12) * 32 / 255 - 23;
263: nMaxLocal = nUsable - 35;
264: }else{ /* Index interior and leaf nodes */
265: nMinLocal = (nUsable - 12) * 32 / 255 - 23;
266: nMaxLocal = (nUsable - 12) * 64 / 255 - 23;
267: }
268:
269: nLocal = nMinLocal + (nTotal - nMinLocal) % (nUsable - 4);
270: if( nLocal>nMaxLocal ) nLocal = nMinLocal;
271: *pnLocal = nLocal;
272: }
273:
274: static int statDecodePage(Btree *pBt, StatPage *p){
275: int nUnused;
276: int iOff;
277: int nHdr;
278: int isLeaf;
279: int szPage;
280:
281: u8 *aData = sqlite3PagerGetData(p->pPg);
282: u8 *aHdr = &aData[p->iPgno==1 ? 100 : 0];
283:
284: p->flags = aHdr[0];
285: p->nCell = get2byte(&aHdr[3]);
286: p->nMxPayload = 0;
287:
288: isLeaf = (p->flags==0x0A || p->flags==0x0D);
289: nHdr = 12 - isLeaf*4 + (p->iPgno==1)*100;
290:
291: nUnused = get2byte(&aHdr[5]) - nHdr - 2*p->nCell;
292: nUnused += (int)aHdr[7];
293: iOff = get2byte(&aHdr[1]);
294: while( iOff ){
295: nUnused += get2byte(&aData[iOff+2]);
296: iOff = get2byte(&aData[iOff]);
297: }
298: p->nUnused = nUnused;
299: p->iRightChildPg = isLeaf ? 0 : sqlite3Get4byte(&aHdr[8]);
300: szPage = sqlite3BtreeGetPageSize(pBt);
301:
302: if( p->nCell ){
303: int i; /* Used to iterate through cells */
304: int nUsable = szPage - sqlite3BtreeGetReserve(pBt);
305:
306: p->aCell = sqlite3_malloc((p->nCell+1) * sizeof(StatCell));
307: memset(p->aCell, 0, (p->nCell+1) * sizeof(StatCell));
308:
309: for(i=0; i<p->nCell; i++){
310: StatCell *pCell = &p->aCell[i];
311:
312: iOff = get2byte(&aData[nHdr+i*2]);
313: if( !isLeaf ){
314: pCell->iChildPg = sqlite3Get4byte(&aData[iOff]);
315: iOff += 4;
316: }
317: if( p->flags==0x05 ){
318: /* A table interior node. nPayload==0. */
319: }else{
320: u32 nPayload; /* Bytes of payload total (local+overflow) */
321: int nLocal; /* Bytes of payload stored locally */
322: iOff += getVarint32(&aData[iOff], nPayload);
323: if( p->flags==0x0D ){
324: u64 dummy;
325: iOff += sqlite3GetVarint(&aData[iOff], &dummy);
326: }
327: if( nPayload>p->nMxPayload ) p->nMxPayload = nPayload;
328: getLocalPayload(nUsable, p->flags, nPayload, &nLocal);
329: pCell->nLocal = nLocal;
330: assert( nPayload>=nLocal );
331: assert( nLocal<=(nUsable-35) );
332: if( nPayload>nLocal ){
333: int j;
334: int nOvfl = ((nPayload - nLocal) + nUsable-4 - 1) / (nUsable - 4);
335: pCell->nLastOvfl = (nPayload-nLocal) - (nOvfl-1) * (nUsable-4);
336: pCell->nOvfl = nOvfl;
337: pCell->aOvfl = sqlite3_malloc(sizeof(u32)*nOvfl);
338: pCell->aOvfl[0] = sqlite3Get4byte(&aData[iOff+nLocal]);
339: for(j=1; j<nOvfl; j++){
340: int rc;
341: u32 iPrev = pCell->aOvfl[j-1];
342: DbPage *pPg = 0;
343: rc = sqlite3PagerGet(sqlite3BtreePager(pBt), iPrev, &pPg);
344: if( rc!=SQLITE_OK ){
345: assert( pPg==0 );
346: return rc;
347: }
348: pCell->aOvfl[j] = sqlite3Get4byte(sqlite3PagerGetData(pPg));
349: sqlite3PagerUnref(pPg);
350: }
351: }
352: }
353: }
354: }
355:
356: return SQLITE_OK;
357: }
358:
359: /*
360: ** Populate the pCsr->iOffset and pCsr->szPage member variables. Based on
361: ** the current value of pCsr->iPageno.
362: */
363: static void statSizeAndOffset(StatCursor *pCsr){
364: StatTable *pTab = (StatTable *)((sqlite3_vtab_cursor *)pCsr)->pVtab;
365: Btree *pBt = pTab->db->aDb[0].pBt;
366: Pager *pPager = sqlite3BtreePager(pBt);
367: sqlite3_file *fd;
368: sqlite3_int64 x[2];
369:
370: /* The default page size and offset */
371: pCsr->szPage = sqlite3BtreeGetPageSize(pBt);
372: pCsr->iOffset = (i64)pCsr->szPage * (pCsr->iPageno - 1);
373:
374: /* If connected to a ZIPVFS backend, override the page size and
375: ** offset with actual values obtained from ZIPVFS.
376: */
377: fd = sqlite3PagerFile(pPager);
378: x[0] = pCsr->iPageno;
379: if( sqlite3OsFileControl(fd, 230440, &x)==SQLITE_OK ){
380: pCsr->iOffset = x[0];
381: pCsr->szPage = x[1];
382: }
383: }
384:
385: /*
386: ** Move a statvfs cursor to the next entry in the file.
387: */
388: static int statNext(sqlite3_vtab_cursor *pCursor){
389: int rc;
390: int nPayload;
391: StatCursor *pCsr = (StatCursor *)pCursor;
392: StatTable *pTab = (StatTable *)pCursor->pVtab;
393: Btree *pBt = pTab->db->aDb[0].pBt;
394: Pager *pPager = sqlite3BtreePager(pBt);
395:
396: sqlite3_free(pCsr->zPath);
397: pCsr->zPath = 0;
398:
399: if( pCsr->aPage[0].pPg==0 ){
400: rc = sqlite3_step(pCsr->pStmt);
401: if( rc==SQLITE_ROW ){
402: int nPage;
403: u32 iRoot = sqlite3_column_int64(pCsr->pStmt, 1);
404: sqlite3PagerPagecount(pPager, &nPage);
405: if( nPage==0 ){
406: pCsr->isEof = 1;
407: return sqlite3_reset(pCsr->pStmt);
408: }
409: rc = sqlite3PagerGet(pPager, iRoot, &pCsr->aPage[0].pPg);
410: pCsr->aPage[0].iPgno = iRoot;
411: pCsr->aPage[0].iCell = 0;
412: pCsr->aPage[0].zPath = sqlite3_mprintf("/");
413: pCsr->iPage = 0;
414: }else{
415: pCsr->isEof = 1;
416: return sqlite3_reset(pCsr->pStmt);
417: }
418: }else{
419:
420: /* Page p itself has already been visited. */
421: StatPage *p = &pCsr->aPage[pCsr->iPage];
422:
423: while( p->iCell<p->nCell ){
424: StatCell *pCell = &p->aCell[p->iCell];
425: if( pCell->iOvfl<pCell->nOvfl ){
426: int nUsable = sqlite3BtreeGetPageSize(pBt)-sqlite3BtreeGetReserve(pBt);
427: pCsr->zName = (char *)sqlite3_column_text(pCsr->pStmt, 0);
428: pCsr->iPageno = pCell->aOvfl[pCell->iOvfl];
429: pCsr->zPagetype = "overflow";
430: pCsr->nCell = 0;
431: pCsr->nMxPayload = 0;
432: pCsr->zPath = sqlite3_mprintf(
433: "%s%.3x+%.6x", p->zPath, p->iCell, pCell->iOvfl
434: );
435: if( pCell->iOvfl<pCell->nOvfl-1 ){
436: pCsr->nUnused = 0;
437: pCsr->nPayload = nUsable - 4;
438: }else{
439: pCsr->nPayload = pCell->nLastOvfl;
440: pCsr->nUnused = nUsable - 4 - pCsr->nPayload;
441: }
442: pCell->iOvfl++;
443: statSizeAndOffset(pCsr);
444: return SQLITE_OK;
445: }
446: if( p->iRightChildPg ) break;
447: p->iCell++;
448: }
449:
450: while( !p->iRightChildPg || p->iCell>p->nCell ){
451: statClearPage(p);
452: if( pCsr->iPage==0 ) return statNext(pCursor);
453: pCsr->iPage--;
454: p = &pCsr->aPage[pCsr->iPage];
455: }
456: pCsr->iPage++;
457: assert( p==&pCsr->aPage[pCsr->iPage-1] );
458:
459: if( p->iCell==p->nCell ){
460: p[1].iPgno = p->iRightChildPg;
461: }else{
462: p[1].iPgno = p->aCell[p->iCell].iChildPg;
463: }
464: rc = sqlite3PagerGet(pPager, p[1].iPgno, &p[1].pPg);
465: p[1].iCell = 0;
466: p[1].zPath = sqlite3_mprintf("%s%.3x/", p->zPath, p->iCell);
467: p->iCell++;
468: }
469:
470:
471: /* Populate the StatCursor fields with the values to be returned
472: ** by the xColumn() and xRowid() methods.
473: */
474: if( rc==SQLITE_OK ){
475: int i;
476: StatPage *p = &pCsr->aPage[pCsr->iPage];
477: pCsr->zName = (char *)sqlite3_column_text(pCsr->pStmt, 0);
478: pCsr->iPageno = p->iPgno;
479:
480: statDecodePage(pBt, p);
481: statSizeAndOffset(pCsr);
482:
483: switch( p->flags ){
484: case 0x05: /* table internal */
485: case 0x02: /* index internal */
486: pCsr->zPagetype = "internal";
487: break;
488: case 0x0D: /* table leaf */
489: case 0x0A: /* index leaf */
490: pCsr->zPagetype = "leaf";
491: break;
492: default:
493: pCsr->zPagetype = "corrupted";
494: break;
495: }
496: pCsr->nCell = p->nCell;
497: pCsr->nUnused = p->nUnused;
498: pCsr->nMxPayload = p->nMxPayload;
499: pCsr->zPath = sqlite3_mprintf("%s", p->zPath);
500: nPayload = 0;
501: for(i=0; i<p->nCell; i++){
502: nPayload += p->aCell[i].nLocal;
503: }
504: pCsr->nPayload = nPayload;
505: }
506:
507: return rc;
508: }
509:
510: static int statEof(sqlite3_vtab_cursor *pCursor){
511: StatCursor *pCsr = (StatCursor *)pCursor;
512: return pCsr->isEof;
513: }
514:
515: static int statFilter(
516: sqlite3_vtab_cursor *pCursor,
517: int idxNum, const char *idxStr,
518: int argc, sqlite3_value **argv
519: ){
520: StatCursor *pCsr = (StatCursor *)pCursor;
521:
522: statResetCsr(pCsr);
523: return statNext(pCursor);
524: }
525:
526: static int statColumn(
527: sqlite3_vtab_cursor *pCursor,
528: sqlite3_context *ctx,
529: int i
530: ){
531: StatCursor *pCsr = (StatCursor *)pCursor;
532: switch( i ){
533: case 0: /* name */
534: sqlite3_result_text(ctx, pCsr->zName, -1, SQLITE_STATIC);
535: break;
536: case 1: /* path */
537: sqlite3_result_text(ctx, pCsr->zPath, -1, SQLITE_TRANSIENT);
538: break;
539: case 2: /* pageno */
540: sqlite3_result_int64(ctx, pCsr->iPageno);
541: break;
542: case 3: /* pagetype */
543: sqlite3_result_text(ctx, pCsr->zPagetype, -1, SQLITE_STATIC);
544: break;
545: case 4: /* ncell */
546: sqlite3_result_int(ctx, pCsr->nCell);
547: break;
548: case 5: /* payload */
549: sqlite3_result_int(ctx, pCsr->nPayload);
550: break;
551: case 6: /* unused */
552: sqlite3_result_int(ctx, pCsr->nUnused);
553: break;
554: case 7: /* mx_payload */
555: sqlite3_result_int(ctx, pCsr->nMxPayload);
556: break;
557: case 8: /* pgoffset */
558: sqlite3_result_int64(ctx, pCsr->iOffset);
559: break;
560: case 9: /* pgsize */
561: sqlite3_result_int(ctx, pCsr->szPage);
562: break;
563: }
564: return SQLITE_OK;
565: }
566:
567: static int statRowid(sqlite3_vtab_cursor *pCursor, sqlite_int64 *pRowid){
568: StatCursor *pCsr = (StatCursor *)pCursor;
569: *pRowid = pCsr->iPageno;
570: return SQLITE_OK;
571: }
572:
573: int sqlite3_dbstat_register(sqlite3 *db){
574: static sqlite3_module dbstat_module = {
575: 0, /* iVersion */
576: statConnect, /* xCreate */
577: statConnect, /* xConnect */
578: statBestIndex, /* xBestIndex */
579: statDisconnect, /* xDisconnect */
580: statDisconnect, /* xDestroy */
581: statOpen, /* xOpen - open a cursor */
582: statClose, /* xClose - close a cursor */
583: statFilter, /* xFilter - configure scan constraints */
584: statNext, /* xNext - advance a cursor */
585: statEof, /* xEof - check for end of scan */
586: statColumn, /* xColumn - read data */
587: statRowid, /* xRowid - read data */
588: 0, /* xUpdate */
589: 0, /* xBegin */
590: 0, /* xSync */
591: 0, /* xCommit */
592: 0, /* xRollback */
593: 0, /* xFindMethod */
594: 0, /* xRename */
595: };
596: sqlite3_create_module(db, "dbstat", &dbstat_module, 0);
597: return SQLITE_OK;
598: }
599:
600: #endif
601:
602: #if defined(SQLITE_TEST) || TCLSH==2
603: #include <tcl.h>
604:
605: static int test_dbstat(
606: void *clientData,
607: Tcl_Interp *interp,
608: int objc,
609: Tcl_Obj *CONST objv[]
610: ){
611: #ifdef SQLITE_OMIT_VIRTUALTABLE
612: Tcl_AppendResult(interp, "dbstat not available because of "
613: "SQLITE_OMIT_VIRTUALTABLE", (void*)0);
614: return TCL_ERROR;
615: #else
616: struct SqliteDb { sqlite3 *db; };
617: char *zDb;
618: Tcl_CmdInfo cmdInfo;
619:
620: if( objc!=2 ){
621: Tcl_WrongNumArgs(interp, 1, objv, "DB");
622: return TCL_ERROR;
623: }
624:
625: zDb = Tcl_GetString(objv[1]);
626: if( Tcl_GetCommandInfo(interp, zDb, &cmdInfo) ){
627: sqlite3* db = ((struct SqliteDb*)cmdInfo.objClientData)->db;
628: sqlite3_dbstat_register(db);
629: }
630: return TCL_OK;
631: #endif
632: }
633:
634: int SqlitetestStat_Init(Tcl_Interp *interp){
635: Tcl_CreateObjCommand(interp, "register_dbstat_vtab", test_dbstat, 0, 0);
636: return TCL_OK;
637: }
638: #endif /* if defined(SQLITE_TEST) || TCLSH==2 */
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>