1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-27 20:41:58 +03:00

tcl extension: UDFs may now 'break' to return an SQL NULL. Add the (eval -asdict) flag to use a dict, instead of an array, for the eval row data.

FossilOrigin-Name: 413a626b5c7902c1810142536c36e4ea8ee7c616ea82dfe1114199f9319091f7
This commit is contained in:
stephan
2025-05-31 11:02:06 +00:00
parent b504aab848
commit 10206572b6
4 changed files with 213 additions and 94 deletions

View File

@ -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.

View File

@ -1 +1 @@
a98a2f49355ec39c56e571c70d377675b1bd99a6d43cf9217b0eb1e081895d8e
413a626b5c7902c1810142536c36e4ea8ee7c616ea82dfe1114199f9319091f7

View File

@ -49,6 +49,10 @@
# 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);
@ -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
){
Tcl_UnsetVar2(interp, Tcl_GetString(pArray),
/* 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{
Tcl_ObjSetVar2(interp, pArray, apColName[i], dbEvalColumnValue(p,i), 0);
/* 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{
/* 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);
}
}
@ -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;
}
@ -2877,7 +2933,7 @@ deserialize_error:
}
if( objc<3 || objc>5 ){
Tcl_WrongNumArgs(interp, 2, objv,
"?OPTIONS? SQL ?ARRAY-NAME? ?SCRIPT?");
"?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;

View File

@ -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 {
@ -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}
@ -848,14 +861,62 @@ 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 }
@ -864,6 +925,7 @@ do_test 20.1 {
} {1 {invalid command name "db"}}
proc closedb {} {
db close
return 10
@ -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