From: drh <> Date: Tue, 14 Apr 2026 11:25:22 +0000 (+0000) Subject: Initial prototype implementation of a new run-time loadable SQL function X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=64a123a21686a70a2c8071e9cd54cbef344c6e7f;p=thirdparty%2Fsqlite.git Initial prototype implementation of a new run-time loadable SQL function to do approximately the same work as sqlite3_analyze. FossilOrigin-Name: 03934b5a6c88a97117a4743a68ee36d30bcbdcebcf11342d30ac42daf9f10874 --- diff --git a/ext/misc/analyze.c b/ext/misc/analyze.c new file mode 100644 index 0000000000..844bce2b1a --- /dev/null +++ b/ext/misc/analyze.c @@ -0,0 +1,293 @@ +/* +** 2026-04-13 +** +** 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. +** +****************************************************************************** +** +** Partial reimplement of the sqlite3_analyzer utility program as +** loadable SQL function. +*/ +#include "sqlite3ext.h" +SQLITE_EXTENSION_INIT1 +#include +#include + +/* +** State information for the analysis +*/ +typedef struct Analysis Analysis; +struct Analysis { + sqlite3 *db; /* Database connection */ + sqlite3_context *context; /* SQL function context */ + sqlite3_str *pOut; /* Write output here */ + char *zSU; /* Name of the temp.space_used table */ + const char *zSchema; /* Schema to be analyzed */ +}; + +/* +** Free all memory associated with the Analysis objecct +*/ +static void analysisFree(Analysis *p){ + if( p->zSU ){ + char *zSql = sqlite3_mprintf("DROP TABLE temp.%s;", p->zSU); + if( zSql ){ + sqlite3_exec(p->db, zSql, 0, 0, 0); + sqlite3_free(zSql); + } + } + sqlite3_str_free(p->pOut); + sqlite3_free(p->zSU); +} + +/* +** Report an OOM +*/ +static void analysisOom(Analysis *p){ + sqlite3_result_error_nomem(p->context); + analysisFree(p); +} + +/* +** Prepare and return an SQL statement. +*/ +static sqlite3_stmt *analysisVPrep(Analysis *p, const char *zFmt, va_list ap){ + char *zSql; + int rc; + sqlite3_stmt *pStmt = 0; + zSql = sqlite3_vmprintf(zFmt, ap); + if( zSql==0 ){ analysisOom(p); return 0; } + rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); + sqlite3_free(zSql); + if( rc ){ + sqlite3_result_error(p->context, "SQL error", -1); + sqlite3_finalize(pStmt); + analysisFree(p); + return 0; + } + return pStmt; +} +static sqlite3_stmt *analysisPrepare(Analysis *p, const char *zFormat, ...){ + va_list ap; + sqlite3_stmt *pStmt = 0; + va_start(ap, zFormat); + pStmt = analysisVPrep(p,zFormat,ap); + va_end(ap); + return pStmt; +} + +/* +** Run SQL. Return the number of errors. +*/ +static int analysisSql(Analysis *p, const char *zFormat, ...){ + va_list ap; + int rc; + sqlite3_stmt *pStmt = 0; + va_start(ap, zFormat); + pStmt = analysisVPrep(p,zFormat,ap); + va_end(ap); + if( pStmt==0 ) return 1; + while( (rc = sqlite3_step(pStmt))==SQLITE_ROW ){} + return sqlite3_finalize(pStmt); +} + +/* +** SQL Function: analyze(SCHEMA) +** +** Analyze the database schema named in the argument. Return text +** containing the analysis. +*/ +static void analyzeFunc( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + int rc; + sqlite3_stmt *pStmt; + int n; + Analysis s; + sqlite3_uint64 r[2]; + + memset(&s, 0, sizeof(s)); + s.db = sqlite3_context_db_handle(context); + s.context = context; + s.pOut = sqlite3_str_new(0); + if( s.pOut==0 ){ analysisOom(&s); return; } + s.zSchema = (const char*)sqlite3_value_text(argv[0]); + sqlite3_randomness(sizeof(r), &r); + s.zSU = sqlite3_mprintf("analysis%016x%016x", r[0], r[1]); + if( s.zSU==0 ){ analysisOom(&s); return; } + + /* The s.zSU table contains the data used for the analysis. + ** The table name contains 128-bits of randomness to avoid + ** collisions with preexisting tables in temp. + */ + rc = analysisSql(&s, + "CREATE TABLE temp.%s(\n" + " name text, -- A table or index\n" + " tblname text, -- Table that owns name\n" + " is_index boolean, -- TRUE if it is an index\n" + " is_without_rowid boolean, -- TRUE if WITHOUT ROWID table\n" + " nentry int, -- Number of entries in the BTree\n" + " leaf_entries int, -- Number of leaf entries\n" + " depth int, -- Depth of the b-tree\n" + " payload int, -- Total data stored in this table/index\n" + " ovfl_payload int, -- Total data stored on overflow pages\n" + " ovfl_cnt int, -- Number of entries that use overflow\n" + " mx_payload int, -- Maximum payload size\n" + " int_pages int, -- Interior pages used\n" + " leaf_pages int, -- Leaf pages used\n" + " ovfl_pages int, -- Overflow pages used\n" + " int_unused int, -- Unused bytes on interior pages\n" + " leaf_unused int, -- Unused bytes on primary pages\n" + " ovfl_unused int -- Unused bytes on overflow pages\n" + ");", + s.zSU + ); + if( rc ) return; + + /* Populate the s.zSU table + */ + rc = analysisSql(&s, + "WITH\n" + " allidx(idxname) AS (\n" + " SELECT name FROM main.sqlite_schema WHERE type='index'\n" + " ),\n" + " allobj(allname,tblname,isidx,isworowid) AS (\n" + " SELECT 'sqlite_schema',\n" + " 'sqlite_schema',\n" + " 0,\n" + " 0\n" + " UNION ALL\n" + " SELECT name,\n" + " tbl_name,\n" + " type='index',\n" + " EXISTS(SELECT 1\n" + " FROM pragma_index_list(sqlite_schema.name,%Q)\n" + " WHERE pragma_index_list.origin='pk'\n" + " AND pragma_index_list.name NOT IN allidx)\n" + " FROM \"%w\".sqlite_schema\n" + " )\n" + "INSERT INTO temp.%s\n" + " SELECT\n" + " allname,\n" + " tblname,\n" + " isidx,\n" + " isworowid,\n" + " sum(ncell),\n" + " sum((pagetype='leaf')*ncell),\n" + " max((length(if(path GLOB '*+*','',path))+3)/4),\n" + " sum(payload),\n" + " sum((pagetype='overflow')*payload),\n" + " sum(path GLOB '*+000000'),\n" + " max(mx_payload),\n" + " sum(pagetype='internal'),\n" + " sum(pagetype='leaf'),\n" + " sum(pagetype='overflow'),\n" + " sum((pagetype='internal')*unused),\n" + " sum((pagetype='leaf')*unused),\n" + " sum((pagetype='overflow')*unused)\n" + " FROM allobj CROSS JOIN dbstat(%Q) \n" + " WHERE dbstat.name=allobj.allname\n" + " GROUP BY allname;\n", + s.zSchema, /* pragma_index_list(...,%Q) */ + s.zSchema, /* %w.sqlite_schema */ + s.zSU, /* INTO temp.%s */ + s.zSchema /* JOIN dbstat(%Q) */ + ); + if( rc ) return; + + /* TBD: Generate report text here */ + + /* Append SQL statements that will recreate the raw data used for + ** the analysis. + */ + sqlite3_str_appendf(s.pOut, + "BEGIN;\n" + "CREATE TABLE space_used(\n" + " name text, -- A table or index\n" /* 0 */ + " tblname text, -- Table that owns name\n" /* 1 */ + " is_index boolean, -- TRUE if it is an index\n" /* 2 */ + " is_without_rowid boolean, -- TRUE if WITHOUT ROWID table\n" /* 3 */ + " nentry int, -- Number of entries in the BTree\n" /* 4 */ + " leaf_entries int, -- Number of leaf entries\n" /* 5 */ + " depth int, -- Depth of the b-tree\n" /* 6 */ + " payload int, -- Total data in this table/index\n" /* 7 */ + " ovfl_payload int, -- Total data on overflow pages\n" /* 8 */ + " ovfl_cnt int, -- Entries that use overflow\n" /* 9 */ + " mx_payload int, -- Maximum payload size\n" /* 10 */ + " int_pages int, -- Interior pages used\n" /* 11 */ + " leaf_pages int, -- Leaf pages used\n" /* 12 */ + " ovfl_pages int, -- Overflow pages used\n" /* 13 */ + " int_unused int, -- Unused bytes on interior pages\n" /* 14 */ + " leaf_unused int, -- Unused bytes on primary pages\n" /* 15 */ + " ovfl_unused int -- Unused bytes on overflow pages\n" /* 16 */ + ");\n" + "INSERT INTO space_used VALUES\n" + ); + pStmt = analysisPrepare(&s, + "SELECT quote(name), quote(tblname),\n" /* 0..1 */ + " is_index, is_without_rowid, nentry, leaf_entries,\n" /* 2..5 */ + " depth, payload, ovfl_payload, ovfl_cnt, mx_payload,\n" /* 6..10 */ + " int_pages, leaf_pages, ovfl_pages, int_unused,\n" /* 11..14 */ + " leaf_unused, ovfl_unused\n" /* 15..16 */ + " FROM temp.%s;", + s.zSU); + if( pStmt==0 ) return; + n = 0; + while( sqlite3_step(pStmt)==SQLITE_ROW ){ + if( n++ ) sqlite3_str_appendf(s.pOut,",\n"); + sqlite3_str_appendf(s.pOut, + " (%s,%s,%lld,%lld,%lld,%lld,%lld,%lld,%lld," + "%lld,%lld,%lld,%lld,%lld,%lld,%lld,%lld)", + sqlite3_column_text(pStmt, 0), + sqlite3_column_text(pStmt, 1), + sqlite3_column_int64(pStmt, 2), + sqlite3_column_int64(pStmt, 3), + sqlite3_column_int64(pStmt, 4), + sqlite3_column_int64(pStmt, 5), + sqlite3_column_int64(pStmt, 6), + sqlite3_column_int64(pStmt, 7), + sqlite3_column_int64(pStmt, 8), + sqlite3_column_int64(pStmt, 9), + sqlite3_column_int64(pStmt, 10), + sqlite3_column_int64(pStmt, 11), + sqlite3_column_int64(pStmt, 12), + sqlite3_column_int64(pStmt, 13), + sqlite3_column_int64(pStmt, 14), + sqlite3_column_int64(pStmt, 15), + sqlite3_column_int64(pStmt, 16)); + } + sqlite3_str_appendf(s.pOut,"\nCOMMIT;\n"); + sqlite3_finalize(pStmt); + + if( sqlite3_str_length(s.pOut) ){ + sqlite3_result_text(context, sqlite3_str_finish(s.pOut), -1, + sqlite3_free); + s.pOut = 0; + } + analysisFree(&s); +} + + +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_analyze_init( + sqlite3 *db, + char **pzErrMsg, + const sqlite3_api_routines *pApi +){ + int rc = SQLITE_OK; + SQLITE_EXTENSION_INIT2(pApi); + (void)pzErrMsg; /* Unused parameter */ + rc = sqlite3_create_function(db, "analyze", 1, + SQLITE_UTF8|SQLITE_INNOCUOUS|SQLITE_DETERMINISTIC, + 0, analyzeFunc, 0, 0); + return rc; +} diff --git a/manifest b/manifest index baa598932a..b197c58f3d 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Minor\sREADME\stypo\sfix\sreported\soff-list\sby\sBrickViking. -D 2026-04-14T02:09:43.838 +C Initial\sprototype\simplementation\sof\sa\snew\srun-time\sloadable\sSQL\sfunction\nto\sdo\sapproximately\sthe\ssame\swork\sas\ssqlite3_analyze. +D 2026-04-14T11:25:22.838 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -358,6 +358,7 @@ F ext/jni/src/tests/000-001-ignored.test e17e874c6ab3c437f1293d88093cf06286083b6 F ext/jni/src/tests/900-001-fts.test bf0ce17a8d082773450e91f2388f5bbb2dfa316d0b676c313c637a91198090f0 F ext/misc/README.md 6243cdc4d7eb791c41ef0716f3980b8b5f6aa8c61ff76a3958cbf0031c6ebfa7 F ext/misc/amatch.c 8d237cc014b3736922c26a76a451050d244aa4980c47c531f368f817b1e77b49 +F ext/misc/analyze.c 09c84bae0688265dd58713b215804f8650dcbe4005a0f0ff44619f7ffdfa9885 F ext/misc/anycollseq.c 5ffdfde9829eeac52219136ad6aa7cd9a4edb3b15f4f2532de52f4a22525eddb F ext/misc/appendvfs.c 9642c7a194a2a25dca7ad3e36af24a0a46d7702168c4ad7e59c9f9b0e16a3824 F ext/misc/base64.c 1445761667c16356e827fc6418294c869468be934429aaa8315035e76dd58acf @@ -2198,8 +2199,11 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P a6c2d7ce84f4f5f8a82cc566a27f98f5418f251e87f1f264ae57935fb10399f7 -R c5e9644977053976d7f63b6d59280f7b -U stephan -Z 9794e2a19f35a12deedc87498af940bf +P aa3432af90b2b9ab0b92f56a8e9c926930b9859e986500bd00d5600c1dd036bf +R 26f98ad980f0da4ded03ad63495bf4ec +T *branch * analyze-sql-func +T *sym-analyze-sql-func * +T -sym-trunk * +U drh +Z 99890306986fdbb1171742fd99ea9c19 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.tags b/manifest.tags index bec971799f..c81c4eaebb 100644 --- a/manifest.tags +++ b/manifest.tags @@ -1,2 +1,2 @@ -branch trunk -tag trunk +branch analyze-sql-func +tag analyze-sql-func diff --git a/manifest.uuid b/manifest.uuid index 495784c43d..47bd1864de 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -aa3432af90b2b9ab0b92f56a8e9c926930b9859e986500bd00d5600c1dd036bf +03934b5a6c88a97117a4743a68ee36d30bcbdcebcf11342d30ac42daf9f10874