]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Initial prototype implementation of a new run-time loadable SQL function
authordrh <>
Tue, 14 Apr 2026 11:25:22 +0000 (11:25 +0000)
committerdrh <>
Tue, 14 Apr 2026 11:25:22 +0000 (11:25 +0000)
to do approximately the same work as sqlite3_analyze.

FossilOrigin-Name: 03934b5a6c88a97117a4743a68ee36d30bcbdcebcf11342d30ac42daf9f10874

ext/misc/analyze.c [new file with mode: 0644]
manifest
manifest.tags
manifest.uuid

diff --git a/ext/misc/analyze.c b/ext/misc/analyze.c
new file mode 100644 (file)
index 0000000..844bce2
--- /dev/null
@@ -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 <assert.h>
+#include <string.h>
+
+/*
+** 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;
+}
index baa598932a180545657fb015084014266c36e29f..b197c58f3d5fad6858b26383d44e1b5bc0b58fe4 100644 (file)
--- 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.
index bec971799ff1b8ee641c166c7aeb22d12c785393..c81c4eaebb7681402276b8953807e5a7add483d3 100644 (file)
@@ -1,2 +1,2 @@
-branch trunk
-tag trunk
+branch analyze-sql-func
+tag analyze-sql-func
index 495784c43d8f65bb3b3d731f7b7456c6823c9535..47bd1864de73c4211e163877555683623d0608aa 100644 (file)
@@ -1 +1 @@
-aa3432af90b2b9ab0b92f56a8e9c926930b9859e986500bd00d5600c1dd036bf
+03934b5a6c88a97117a4743a68ee36d30bcbdcebcf11342d30ac42daf9f10874