]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
tcl extension: UDFs may now 'break' to return an SQL NULL. Add the (eval -asdict...
authorstephan <stephan@noemail.net>
Sat, 31 May 2025 11:02:06 +0000 (11:02 +0000)
committerstephan <stephan@noemail.net>
Sat, 31 May 2025 11:02:06 +0000 (11:02 +0000)
FossilOrigin-Name: 413a626b5c7902c1810142536c36e4ea8ee7c616ea82dfe1114199f9319091f7

manifest
manifest.uuid
src/tclsqlite.c
test/tclsqlite.test

index cb40320603325e45618761dac1024346cbe546c5..9411b20b10eac4c4da34baccfbe596f4f6450030 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Add\ssome\smissing\sUNUSED_PARAMETER()\sannotations\sto\ssquelch\sdownstream\sbuild\swarnings\swhen\susing\s-Wextra\s-pedantic.
-D 2025-05-31T09:44:00.684
+C tcl\sextension:\sUDFs\smay\snow\s'break'\sto\sreturn\san\sSQL\sNULL.\sAdd\sthe\s(eval\s-asdict)\sflag\sto\suse\sa\sdict,\sinstead\sof\san\sarray,\sfor\sthe\seval\srow\sdata.
+D 2025-05-31T11:02:06.099
 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -794,7 +794,7 @@ F src/sqliteInt.h bb9137b860b2416b12788f09b32384ceab96b720aae07a6e9afacc545e4361
 F src/sqliteLimit.h 6d817c28a8f19af95e6f4921933b7fbbca48a962bce0eb0ec81e8bb3ef38e68b
 F src/status.c 0e72e4f6be6ccfde2488eb63210297e75f569f3ce9920f6c3d77590ec6ce5ffd
 F src/table.c 0f141b58a16de7e2fbe81c308379e7279f4c6b50eb08efeec5892794a0ba30d1
-F src/tclsqlite.c d0e63ffe7944dd223bf62066d9f982cbee1978811c7fbfd889f4ba9c5baed3d1
+F src/tclsqlite.c 3c604c49e6cf4211960a9ddb9505280fd22cde32175f40884c641c0f5a286036
 F src/tclsqlite.h 65e2c761446e1c9fa0342b7d2612a703483643c8b6a316d12a65b745a4727395
 F src/test1.c 9b54135e5f1352f06b1d23d7c183f124c1f33de6ea8997cd801f0f215c43591d
 F src/test2.c 62f0830958f9075692c29c6de51b495ae8969e1bef85f239ffcd9ba5fb44a5ff
@@ -1725,7 +1725,7 @@ F test/tabfunc01.test 8a484fe8b19fc24844f72ca1ceb7c9ae8c9a6bca000a5c6ccab5d89f5c
 F test/table.test e87294bf1c80bfd7792142b84ab32ea5beb4f3f71e535d7fb263a6b2068377bf
 F test/tableapi.test e37c33e6be2276e3a96bb54b00eea7f321277115d10e5b30fdb52a112b432750
 F test/tableopts.test dba698ba97251017b7c80d738c198d39ab747930
-F test/tclsqlite.test ad0bbd92edabe64cc91d990a0748142fe5ab962d74ac71fa3bfa94d50d2f4c87
+F test/tclsqlite.test 3f697424cfc1cdc9c076ec0cadb0e700f059400a3e3ce134b7d856fc9f880e1c
 F test/tempdb.test 4cdaa23ddd8acb4d79cbb1b68ccdfd09b0537aaba909ca69a876157c2a2cbd08
 F test/tempdb2.test 353864e96fd3ae2f70773d0ffbf8b1fe48589b02c2ec05013b540879410c3440
 F test/tempfault.test 0c0d349c9a99bf5f374655742577f8712c647900
@@ -2207,8 +2207,8 @@ F tool/version-info.c 3b36468a90faf1bbd59c65fd0eb66522d9f941eedd364fabccd7227350
 F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7
 F tool/warnings.sh 1ad0169b022b280bcaaf94a7fa231591be96b514230ab5c98fbf15cd7df842dd
 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
-P cf5b37b3a39013d8ca9de92da2289346caf52b56daff59e19b140cc586a3421f
-R bf16fd55e66316305a43e4a57281b369
+P a98a2f49355ec39c56e571c70d377675b1bd99a6d43cf9217b0eb1e081895d8e
+R b5e249a32712c6263d2ec9033caa9f81
 U stephan
-Z 52f694bf6ce8c40102964ca1cfc5e8c9
+Z 24f79bc914343b0b71f5079a7d4e350f
 # Remove this line to create a well-formed Fossil manifest.
index 941391085bbc0a35c2a9f2c79300f48d113216c9..df508c2ee787cc65f248895457a4c9502a569444 100644 (file)
@@ -1 +1 @@
-a98a2f49355ec39c56e571c70d377675b1bd99a6d43cf9217b0eb1e081895d8e
+413a626b5c7902c1810142536c36e4ea8ee7c616ea82dfe1114199f9319091f7
index 8c40b869222600fe3d3a92cf35eb5c66602545e3..02a4d84e46c1600cc32044791d8cef356c8dc15e 100644 (file)
 # define CONST const
 #elif !defined(Tcl_Size)
   typedef int Tcl_Size;
+# ifndef Tcl_BounceRefCount
+#  define Tcl_BounceRefCount(X) Tcl_IncrRefCount(X); Tcl_DecrRefCount(X)
+   /* https://www.tcl-lang.org/man/tcl9.0/TclLib/Object.html */
+# endif
 #endif
 /**** End copy of tclsqlite.h ****/
 
@@ -1084,7 +1088,9 @@ static void tclSqlFunc(sqlite3_context *context, int argc, sqlite3_value**argv){
     Tcl_DecrRefCount(pCmd);
   }
 
-  if( rc && rc!=TCL_RETURN ){
+  if( TCL_BREAK==rc ){
+    sqlite3_result_null(context);
+  }else if( rc && rc!=TCL_RETURN ){
     sqlite3_result_error(context, Tcl_GetStringResult(p->interp), -1);
   }else{
     Tcl_Obj *pVar = Tcl_GetObjResult(p->interp);
@@ -1102,7 +1108,7 @@ static void tclSqlFunc(sqlite3_context *context, int argc, sqlite3_value**argv){
       }else if( (c=='b' && pVar->bytes==0 && strcmp(zType,"boolean")==0 )
              || (c=='b' && pVar->bytes==0 && strcmp(zType,"booleanString")==0 )
              || (c=='w' && strcmp(zType,"wideInt")==0)
-             || (c=='i' && strcmp(zType,"int")==0) 
+             || (c=='i' && strcmp(zType,"int")==0)
       ){
         eType = SQLITE_INTEGER;
       }else if( c=='d' && strcmp(zType,"double")==0 ){
@@ -1616,11 +1622,12 @@ struct DbEvalContext {
   SqlPreparedStmt *pPreStmt;      /* Current statement */
   int nCol;                       /* Number of columns returned by pStmt */
   int evalFlags;                  /* Flags used */
-  Tcl_Obj *pArray;                /* Name of array variable */
+  Tcl_Obj *pVarName;              /* Name of target array/dict variable */
   Tcl_Obj **apColName;            /* Array of column names */
 };
 
 #define SQLITE_EVAL_WITHOUTNULLS  0x00001  /* Unset array(*) for NULL */
+#define SQLITE_EVAL_ASDICT        0x00002  /* Use dict instead of array */
 
 /*
 ** Release any cache of column names currently held as part of
@@ -1641,20 +1648,20 @@ static void dbReleaseColumnNames(DbEvalContext *p){
 /*
 ** Initialize a DbEvalContext structure.
 **
-** If pArray is not NULL, then it contains the name of a Tcl array
+** If pVarName is not NULL, then it contains the name of a Tcl array
 ** variable. The "*" member of this array is set to a list containing
 ** the names of the columns returned by the statement as part of each
 ** call to dbEvalStep(), in order from left to right. e.g. if the names
 ** of the returned columns are a, b and c, it does the equivalent of the
 ** tcl command:
 **
-**     set ${pArray}(*) {a b c}
+**     set ${pVarName}(*) {a b c}
 */
 static void dbEvalInit(
   DbEvalContext *p,               /* Pointer to structure to initialize */
   SqliteDb *pDb,                  /* Database handle */
   Tcl_Obj *pSql,                  /* Object containing SQL script */
-  Tcl_Obj *pArray,                /* Name of Tcl array to set (*) element of */
+  Tcl_Obj *pVarName,              /* Name of Tcl array to set (*) element of */
   int evalFlags                   /* Flags controlling evaluation */
 ){
   memset(p, 0, sizeof(DbEvalContext));
@@ -1662,9 +1669,9 @@ static void dbEvalInit(
   p->zSql = Tcl_GetString(pSql);
   p->pSql = pSql;
   Tcl_IncrRefCount(pSql);
-  if( pArray ){
-    p->pArray = pArray;
-    Tcl_IncrRefCount(pArray);
+  if( pVarName ){
+    p->pVarName = pVarName;
+    Tcl_IncrRefCount(pVarName);
   }
   p->evalFlags = evalFlags;
   addDatabaseRef(p->pDb);
@@ -1687,7 +1694,7 @@ static void dbEvalRowInfo(
     Tcl_Obj **apColName = 0;      /* Array of column names */
 
     p->nCol = nCol = sqlite3_column_count(pStmt);
-    if( nCol>0 && (papColName || p->pArray) ){
+    if( nCol>0 && (papColName || p->pVarName) ){
       apColName = (Tcl_Obj**)Tcl_Alloc( sizeof(Tcl_Obj*)*nCol );
       for(i=0; i<nCol; i++){
         apColName[i] = Tcl_NewStringObj(sqlite3_column_name(pStmt,i), -1);
@@ -1696,20 +1703,35 @@ static void dbEvalRowInfo(
       p->apColName = apColName;
     }
 
-    /* If results are being stored in an array variable, then create
-    ** the array(*) entry for that array
+    /* If results are being stored in a variable then create the
+    ** array(*) or dict(*) entry for that variable.
     */
-    if( p->pArray ){
+    if( p->pVarName ){
       Tcl_Interp *interp = p->pDb->interp;
       Tcl_Obj *pColList = Tcl_NewObj();
       Tcl_Obj *pStar = Tcl_NewStringObj("*", -1);
 
+      Tcl_IncrRefCount(pColList);
+      Tcl_IncrRefCount(pStar);
       for(i=0; i<nCol; i++){
         Tcl_ListObjAppendElement(interp, pColList, apColName[i]);
       }
-      Tcl_IncrRefCount(pStar);
-      Tcl_ObjSetVar2(interp, p->pArray, pStar, pColList, 0);
+      if( 0==(SQLITE_EVAL_ASDICT & p->evalFlags) ){
+        Tcl_ObjSetVar2(interp, p->pVarName, pStar, pColList, 0);
+      }else{
+        Tcl_Obj * pDict = Tcl_ObjGetVar2(interp, p->pVarName, NULL, 0);
+        if( !pDict ){
+          pDict = Tcl_NewDictObj();
+        }else if( Tcl_IsShared(pDict) ){
+          pDict = Tcl_DuplicateObj(pDict);
+        }
+        if( Tcl_DictObjPut(interp, pDict, pStar, pColList)==TCL_OK ){
+          Tcl_ObjSetVar2(interp, p->pVarName, NULL, pDict, 0);
+        }
+        Tcl_BounceRefCount(pDict);
+      }
       Tcl_DecrRefCount(pStar);
+      Tcl_DecrRefCount(pColList);
     }
   }
 
@@ -1751,7 +1773,7 @@ static int dbEvalStep(DbEvalContext *p){
       if( rcs==SQLITE_ROW ){
         return TCL_OK;
       }
-      if( p->pArray ){
+      if( p->pVarName ){
         dbEvalRowInfo(p, 0, 0);
       }
       rcs = sqlite3_reset(pStmt);
@@ -1802,9 +1824,9 @@ static void dbEvalFinalize(DbEvalContext *p){
     dbReleaseStmt(p->pDb, p->pPreStmt, 0);
     p->pPreStmt = 0;
   }
-  if( p->pArray ){
-    Tcl_DecrRefCount(p->pArray);
-    p->pArray = 0;
+  if( p->pVarName ){
+    Tcl_DecrRefCount(p->pVarName);
+    p->pVarName = 0;
   }
   Tcl_DecrRefCount(p->pSql);
   dbReleaseColumnNames(p);
@@ -1879,7 +1901,7 @@ static int DbUseNre(void){
 /*
 ** This function is part of the implementation of the command:
 **
-**   $db eval SQL ?ARRAYNAME? SCRIPT
+**   $db eval SQL ?TGT-NAME? SCRIPT
 */
 static int SQLITE_TCLAPI DbEvalNextCmd(
   ClientData data[],                   /* data[0] is the (DbEvalContext*) */
@@ -1893,8 +1915,8 @@ static int SQLITE_TCLAPI DbEvalNextCmd(
   ** is a pointer to a Tcl_Obj containing the script to run for each row
   ** returned by the queries encapsulated in data[0]. */
   DbEvalContext *p = (DbEvalContext *)data[0];
-  Tcl_Obj *pScript = (Tcl_Obj *)data[1];
-  Tcl_Obj *pArray = p->pArray;
+  Tcl_Obj * const pScript = (Tcl_Obj *)data[1];
+  Tcl_Obj * const pVarName = p->pVarName;
 
   while( (rc==TCL_OK || rc==TCL_CONTINUE) && TCL_OK==(rc = dbEvalStep(p)) ){
     int i;
@@ -1902,15 +1924,46 @@ static int SQLITE_TCLAPI DbEvalNextCmd(
     Tcl_Obj **apColName;
     dbEvalRowInfo(p, &nCol, &apColName);
     for(i=0; i<nCol; i++){
-      if( pArray==0 ){
+      if( pVarName==0 ){
         Tcl_ObjSetVar2(interp, apColName[i], 0, dbEvalColumnValue(p,i), 0);
       }else if( (p->evalFlags & SQLITE_EVAL_WITHOUTNULLS)!=0
-             && sqlite3_column_type(p->pPreStmt->pStmt, i)==SQLITE_NULL 
+             && sqlite3_column_type(p->pPreStmt->pStmt, i)==SQLITE_NULL
       ){
-        Tcl_UnsetVar2(interp, Tcl_GetString(pArray), 
-                      Tcl_GetString(apColName[i]), 0);
+        /* Remove NULL-containing column from the target container... */
+        if( 0==(SQLITE_EVAL_ASDICT & p->evalFlags) ){
+          /* Target is an array */
+          Tcl_UnsetVar2(interp, Tcl_GetString(pVarName),
+                        Tcl_GetString(apColName[i]), 0);
+        }else{
+          /* Target is a dict */
+          Tcl_Obj *pDict = Tcl_ObjGetVar2(interp, pVarName, NULL, 0);
+          if( pDict ){
+            if( Tcl_IsShared(pDict) ){
+              pDict = Tcl_DuplicateObj(pDict);
+            }
+            if( Tcl_DictObjRemove(interp, pDict, apColName[i])==TCL_OK ){
+              Tcl_ObjSetVar2(interp, pVarName, NULL, pDict, 0);
+            }
+            Tcl_BounceRefCount(pDict);
+          }
+        }
+      }else if( 0==(SQLITE_EVAL_ASDICT & p->evalFlags) ){
+        /* Target is an array: set target(colName) = colValue */
+        Tcl_ObjSetVar2(interp, pVarName, apColName[i],
+                       dbEvalColumnValue(p,i), 0);
       }else{
-        Tcl_ObjSetVar2(interp, pArray, apColName[i], dbEvalColumnValue(p,i), 0);
+        /* Target is a dict: set target(colName) = colValue */
+        Tcl_Obj *pDict = Tcl_ObjGetVar2(interp, pVarName, NULL, 0);
+        if( !pDict ){
+          pDict = Tcl_NewDictObj();
+        }else if( Tcl_IsShared(pDict) ){
+          pDict = Tcl_DuplicateObj(pDict);
+        }
+        if( Tcl_DictObjPut(interp, pDict, apColName[i],
+                           dbEvalColumnValue(p,i))==TCL_OK ){
+          Tcl_ObjSetVar2(interp, pVarName, NULL, pDict, 0);
+        }
+        Tcl_BounceRefCount(pDict);
       }
     }
 
@@ -2019,7 +2072,7 @@ static int SQLITE_TCLAPI DbObjCmd(
     "timeout",                "total_changes",         "trace",
     "trace_v2",               "transaction",           "unlock_notify",
     "update_hook",            "version",               "wal_hook",
-    0                        
+    0
   };
   enum DB_enum {
     DB_AUTHORIZER,            DB_BACKUP,               DB_BIND_FALLBACK,
@@ -2853,13 +2906,15 @@ deserialize_error:
   }
 
   /*
-  **    $db eval ?options? $sql ?array? ?{  ...code... }?
+  **    $db eval ?options? $sql ?varName? ?{  ...code... }?
   **
-  ** The SQL statement in $sql is evaluated.  For each row, the values are
-  ** placed in elements of the array named "array" and ...code... is executed.
-  ** If "array" and "code" are omitted, then no callback is every invoked.
-  ** If "array" is an empty string, then the values are placed in variables
-  ** that have the same name as the fields extracted by the query.
+  ** The SQL statement in $sql is evaluated.  For each row, the values
+  ** are placed in elements of the array or dict named $varName and
+  ** ...code... is executed.  If $varName and $code are omitted, then
+  ** no callback is ever invoked.  If $varName is an empty string,
+  ** then the values are placed in variables that have the same name
+  ** as the fields extracted by the query, and those variables are
+  ** accessible during the eval of $code.
   */
   case DB_EVAL: {
     int evalFlags = 0;
@@ -2867,8 +2922,9 @@ deserialize_error:
     while( objc>3 && (zOpt = Tcl_GetString(objv[2]))!=0 && zOpt[0]=='-' ){
       if( strcmp(zOpt, "-withoutnulls")==0 ){
         evalFlags |= SQLITE_EVAL_WITHOUTNULLS;
-      }
-      else{
+      }else if( strcmp(zOpt, "-asdict")==0 ){
+        evalFlags |= SQLITE_EVAL_ASDICT;
+      }else{
         Tcl_AppendResult(interp, "unknown option: \"", zOpt, "\"", (void*)0);
         return TCL_ERROR;
       }
@@ -2876,8 +2932,8 @@ deserialize_error:
       objv++;
     }
     if( objc<3 || objc>5 ){
-      Tcl_WrongNumArgs(interp, 2, objv, 
-          "?OPTIONS? SQL ?ARRAY-NAME? ?SCRIPT?");
+      Tcl_WrongNumArgs(interp, 2, objv,
+          "?OPTIONS? SQL ?VAR-NAME? ?SCRIPT?");
       return TCL_ERROR;
     }
 
@@ -2903,17 +2959,17 @@ deserialize_error:
     }else{
       ClientData cd2[2];
       DbEvalContext *p;
-      Tcl_Obj *pArray = 0;
+      Tcl_Obj *pVarName = 0;
       Tcl_Obj *pScript;
 
       if( objc>=5 && *(char *)Tcl_GetString(objv[3]) ){
-        pArray = objv[3];
+        pVarName = objv[3];
       }
       pScript = objv[objc-1];
       Tcl_IncrRefCount(pScript);
 
       p = (DbEvalContext *)Tcl_Alloc(sizeof(DbEvalContext));
-      dbEvalInit(p, pDb, objv[2], pArray, evalFlags);
+      dbEvalInit(p, pDb, objv[2], pVarName, evalFlags);
 
       cd2[0] = (void *)p;
       cd2[1] = (void *)pScript;
index 0758abd822e7d305f204504b2ed2530ee306f136..5f373ea18ae1504cae5c89b0df38c73e4a44fcaf 100644 (file)
@@ -9,7 +9,7 @@
 #
 #***********************************************************************
 # This file implements regression tests for TCL interface to the
-# SQLite library. 
+# SQLite library.
 #
 # Actually, all tests are based on the TCL interface, so the main
 # interface is pretty well tested.  This file contains some addition
@@ -121,7 +121,7 @@ ifcapable {complete} {
 do_test tcl-1.14 {
   set v [catch {db eval} msg]
   lappend v $msg
-} {1 {wrong # args: should be "db eval ?OPTIONS? SQL ?ARRAY-NAME? ?SCRIPT?"}}
+} {1 {wrong # args: should be "db eval ?OPTIONS? SQL ?VAR-NAME? ?SCRIPT?"}}
 do_test tcl-1.15 {
   set v [catch {db function} msg]
   lappend v $msg
@@ -359,6 +359,19 @@ do_test tcl-9.3 {
   execsql {SELECT typeof(ret_int())}
 } {integer}
 
+proc breakAsNullUdf args {
+  if {"1" eq [lindex $args 0]} {return -code break}
+}
+do_test tcl-9.4 {
+  db function banu breakAsNullUdf
+  execsql {SELECT typeof(banu()), typeof(banu(1))}
+} {text null}
+do_test tcl-9.5 {
+  db nullvalue banunull
+  db eval {SELECT banu(), banu(1)}
+} {{} banunull}
+
+
 # Recursive calls to the same user-defined function
 #
 ifcapable tclvar {
@@ -465,7 +478,7 @@ do_test tcl-10.13 {
   db eval {SELECT * FROM t4}
 } {1 2 5 6 7}
 
-# Now test that [db transaction] commands may be nested with 
+# Now test that [db transaction] commands may be nested with
 # the expected results.
 #
 do_test tcl-10.14 {
@@ -475,7 +488,7 @@ do_test tcl-10.14 {
       INSERT INTO t4 VALUES('one');
     }
 
-    catch { 
+    catch {
       db transaction {
         db eval { INSERT INTO t4 VALUES('two') }
         db transaction {
@@ -674,11 +687,11 @@ do_test tcl-15.5 {
 } {0}
 
 
-# 2017-06-26: The --withoutnulls flag to "db eval".
+# 2017-06-26: The -withoutnulls flag to "db eval".
 #
-# In the "db eval --withoutnulls SQL ARRAY" form, NULL results cause the
-# corresponding array entry to be unset.  The default behavior (without
-# the -withoutnulls flags) is for the corresponding array value to get
+# In the "db eval -withoutnulls SQL TARGET" form, NULL results cause the
+# corresponding target entry to be unset.  The default behavior (without
+# the -withoutnulls flags) is for the corresponding target value to get
 # the [db nullvalue] string.
 #
 catch {db close}
@@ -720,64 +733,64 @@ reset_db
 proc add {a b} { return [expr $a + $b] }
 proc ret {a} { return $a }
 
-db function add_i -returntype integer add 
+db function add_i -returntype integer add
 db function add_r -ret        real    add
-db function add_t -return     text    add 
-db function add_b -returntype blob    add 
-db function add_a -returntype any     add 
+db function add_t -return     text    add
+db function add_b -returntype blob    add
+db function add_a -returntype any     add
 
-db function ret_i -returntype int     ret 
+db function ret_i -returntype int     ret
 db function ret_r -returntype real    ret
-db function ret_t -returntype text    ret 
-db function ret_b -returntype blob    ret 
-db function ret_a -r          any     ret 
+db function ret_t -returntype text    ret
+db function ret_b -returntype blob    ret
+db function ret_a -r          any     ret
 
 do_execsql_test 17.0 {
   SELECT quote( add_i(2, 3) );
-  SELECT quote( add_r(2, 3) ); 
-  SELECT quote( add_t(2, 3) ); 
-  SELECT quote( add_b(2, 3) ); 
-  SELECT quote( add_a(2, 3) ); 
+  SELECT quote( add_r(2, 3) );
+  SELECT quote( add_t(2, 3) );
+  SELECT quote( add_b(2, 3) );
+  SELECT quote( add_a(2, 3) );
 } {5 5.0 '5' X'35' 5}
 
 do_execsql_test 17.1 {
   SELECT quote( add_i(2.2, 3.3) );
-  SELECT quote( add_r(2.2, 3.3) ); 
-  SELECT quote( add_t(2.2, 3.3) ); 
-  SELECT quote( add_b(2.2, 3.3) ); 
-  SELECT quote( add_a(2.2, 3.3) ); 
+  SELECT quote( add_r(2.2, 3.3) );
+  SELECT quote( add_t(2.2, 3.3) );
+  SELECT quote( add_b(2.2, 3.3) );
+  SELECT quote( add_a(2.2, 3.3) );
 } {5.5 5.5 '5.5' X'352E35' 5.5}
 
 do_execsql_test 17.2 {
   SELECT quote( ret_i(2.5) );
-  SELECT quote( ret_r(2.5) ); 
-  SELECT quote( ret_t(2.5) ); 
-  SELECT quote( ret_b(2.5) ); 
-  SELECT quote( ret_a(2.5) ); 
+  SELECT quote( ret_r(2.5) );
+  SELECT quote( ret_t(2.5) );
+  SELECT quote( ret_b(2.5) );
+  SELECT quote( ret_a(2.5) );
 } {2.5 2.5 '2.5' X'322E35' 2.5}
 
 do_execsql_test 17.3 {
   SELECT quote( ret_i('2.5') );
-  SELECT quote( ret_r('2.5') ); 
-  SELECT quote( ret_t('2.5') ); 
-  SELECT quote( ret_b('2.5') ); 
-  SELECT quote( ret_a('2.5') ); 
+  SELECT quote( ret_r('2.5') );
+  SELECT quote( ret_t('2.5') );
+  SELECT quote( ret_b('2.5') );
+  SELECT quote( ret_a('2.5') );
 } {2.5 2.5 '2.5' X'322E35' '2.5'}
 
 do_execsql_test 17.4 {
   SELECT quote( ret_i('abc') );
-  SELECT quote( ret_r('abc') ); 
-  SELECT quote( ret_t('abc') ); 
-  SELECT quote( ret_b('abc') ); 
-  SELECT quote( ret_a('abc') ); 
+  SELECT quote( ret_r('abc') );
+  SELECT quote( ret_t('abc') );
+  SELECT quote( ret_b('abc') );
+  SELECT quote( ret_a('abc') );
 } {'abc' 'abc' 'abc' X'616263' 'abc'}
 
 do_execsql_test 17.5 {
   SELECT quote( ret_i(X'616263') );
-  SELECT quote( ret_r(X'616263') ); 
-  SELECT quote( ret_t(X'616263') ); 
-  SELECT quote( ret_b(X'616263') ); 
-  SELECT quote( ret_a(X'616263') ); 
+  SELECT quote( ret_r(X'616263') );
+  SELECT quote( ret_t(X'616263') );
+  SELECT quote( ret_b(X'616263') );
+  SELECT quote( ret_a(X'616263') );
 } {'abc' 'abc' 'abc' X'616263' X'616263'}
 
 do_test 17.6.1 {
@@ -848,21 +861,70 @@ do_catchsql_test 19.911 {
 } {1 {invalid command name "bind_fallback_does_not_exist"}}
 db bind_fallback {}
 
-#-------------------------------------------------------------------------
+# 2025-05-05: the -asdict eval flag
+#
 do_test 20.0 {
+  execsql {CREATE TABLE tad(a,b)}
+  execsql {INSERT INTO tad(a,b) VALUES('aa','bb'),('AA','BB')}
+  db eval -asdict {
+    SELECT a, b FROM tad WHERE 0
+  } D {}
+  set D
+} {* {a b}}
+
+do_test 20.1 {
+  unset D
+  set i 0
+  set res {}
+  set colNames {}
+  db eval -asdict {
+    SELECT a, b FROM tad ORDER BY a
+  } D {
+    dict set D i [incr i]
+    lappend res $i [dict get $D a] [dict get $D b]
+    if {1 == $i} {
+      set colNames [dict get $D *]
+    }
+  }
+  lappend res $colNames
+  unset D
+  set res
+} {1 AA BB 2 aa bb {a b}}
+
+do_test 20.2 {
+  set res {}
+  db eval -asdict -withoutnulls {
+    SELECT n, a, b FROM (
+      SELECT 1 as n, 'aa' as a, NULL as b
+      UNION ALL
+      SELECT 2 as n, NULL as a, 'bb' as b
+    )
+    ORDER BY n
+  } D {
+    dict unset D *
+    lappend res [dict values $D]
+  }
+  unset D
+  execsql {DROP TABLE tad}
+  set res
+} {{1 aa} {2 bb}}
+
+#-------------------------------------------------------------------------
+do_test 21.0 {
   db transaction {
     db close
   }
 } {}
 
-do_test 20.1 {
+do_test 21.1 {
   sqlite3 db test.db
   set rc [catch {
     db eval {SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3} { db close }
   } msg]
   list $rc $msg
 } {1 {invalid command name "db"}}
-  
+
+
 
 proc closedb {} {
   db close
@@ -874,7 +936,7 @@ sqlite3 db test.db
 db func closedb closedb
 db func func1 func1
 
-do_test 20.2 {
+do_test 21.2 {
   set rc [catch {
     db eval {
       SELECT closedb(),func1() UNION ALL SELECT 20,30 UNION ALL SELECT 30,40
@@ -884,9 +946,10 @@ do_test 20.2 {
 } {0 {10 1 20 30 30 40}}
 
 sqlite3 db :memory:
-do_test 21.1 {
+do_test 22.1 {
   catch {db eval {SELECT 1 2 3;}} msg
   db erroroffset
 } {9}
 
+
 finish_test