From 08f2d0d53ff77c2c59495fb5ca2cad1615ebc395 Mon Sep 17 00:00:00 2001 From: 4gl <@> Date: Mon, 27 Apr 2026 14:11:09 +0100 Subject: [PATCH 01/13] feat: castabload macro --- tests/viyaonly/mv_castabload.test.sas | 148 ++++++++++++++++++++++++++ viya/mv_castabload.sas | 102 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 tests/viyaonly/mv_castabload.test.sas create mode 100644 viya/mv_castabload.sas diff --git a/tests/viyaonly/mv_castabload.test.sas b/tests/viyaonly/mv_castabload.test.sas new file mode 100644 index 0000000..d8dd200 --- /dev/null +++ b/tests/viyaonly/mv_castabload.test.sas @@ -0,0 +1,148 @@ +/** + @file + @brief Testing mv_castabload macro + +

SAS Macros

+ @li mf_uid.sas + @li mp_assert.sas + @li mp_assertscope.sas + @li mv_castabload.sas + @li mv_createfile.sas + +**/ + +options mprint; + +/* ------------------------------------------------------------------------ */ +/* Setup: start a CAS session and stage a source file in the Public caslib */ +/* ------------------------------------------------------------------------ */ +cas mysess; +caslib _all_ assign; + +%let testcaslib = Public; /* change this if Public isn't available */ +proc cas; + table.caslibInfo result=r / ; + found = 0; + do row over r.CASLibInfo; + if upcase(row.Name) = upcase("&testcaslib") then found = 1; + end; + if found = 0 then do; + print "ERROR: caslib &testcaslib not available"; + exit; + end; +quit; +%put NOTE: Using testcaslib=&testcaslib; + +%let tab1=T%mf_uid(); +%let tab2=T%mf_uid(); +%let tab3=T%mf_uid(); + +/* Create a SASHDAT source file in the Public caslib from SASHELP.BASEBALL + so that subsequent LOAD operations have something real to pick up. */ +proc casutil; + load data=sashelp.baseball outcaslib="&testcaslib" casout="&tab1" replace; + save casdata="&tab1" incaslib="&testcaslib" + casout="&tab1..sashdat" outcaslib="&testcaslib" replace; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; +quit; + +/* And a second hdat, with a name different from the table name, so that we + can exercise the explicit srcfile= path. */ +proc casutil; + load data=sashelp.cars outcaslib="&testcaslib" casout="&tab2" replace; + save casdata="&tab2" incaslib="&testcaslib" + casout="src_&tab2..sashdat" outcaslib="&testcaslib" replace; + droptable casdata="&tab2" incaslib="&testcaslib" quiet; +quit; + + +/* ------------------------------------------------------------------------ */ +%put TEST 1 - missing required parameters returns without setting RC to 0/1; +/* ------------------------------------------------------------------------ */ +%let MV_CASTABLOAD_RC=; +%mv_castabload(caslib=,table=,srcfile=) + +%mp_assert( + iftrue=(&MV_CASTABLOAD_RC=3), + desc=Check RC=3 (initial/failure value) when required params are missing +) + + +/* ------------------------------------------------------------------------ */ +%put TEST 2 - load a table that does not yet exist (default srcfile=table.hdat); +/* ------------------------------------------------------------------------ */ +%mv_castabload(caslib=&testcaslib,table=&tab1,mdebug=1) + +%mp_assert( + iftrue=(&MV_CASTABLOAD_RC=1), + desc=Check RC=1 when table is loaded and promoted for the first time +) + + +/* ------------------------------------------------------------------------ */ +%put TEST 3 - calling again for the same table should be a no-op (RC=0); +/* also verify no scope leakage of macro variables */ +/* ------------------------------------------------------------------------ */ +%mp_assertscope(SNAPSHOT) + +%mv_castabload(caslib=&testcaslib,table=&tab1,mdebug=1) + +%mp_assertscope(COMPARE, + desc=Check mv_castabload does not leak macro variables into GLOBAL scope, + ignorelist=MV_CASTABLOAD_RC +) + +%mp_assert( + iftrue=(&MV_CASTABLOAD_RC=0), + desc=Check RC=0 when table is already in-memory (skip load) +) + + +/* ------------------------------------------------------------------------ */ +%put TEST 4 - explicit srcfile= where file name differs from table name; +/* ------------------------------------------------------------------------ */ +%mv_castabload( + caslib=&testcaslib, + table=&tab2, + srcfile=src_&tab2..sashdat, + mdebug=1 +) + +%mp_assert( + iftrue=(&MV_CASTABLOAD_RC=1), + desc=Check RC=1 when loading with explicit srcfile= parameter +) + + +/* ------------------------------------------------------------------------ */ +%put TEST 5 - load failure when srcfile does not exist in the caslib; +/* ------------------------------------------------------------------------ */ +%mv_castabload( + caslib=&testcaslib, + table=&tab3, + srcfile=doesnotexist_%mf_uid..sashdat, + mdebug=1 +) + +%mp_assert( + iftrue=(&MV_CASTABLOAD_RC=3), + desc=Check RC=3 when source file cannot be found / load fails +) + +/* reset so that a downstream failure RC does not break testterm */ +%let syscc=0; + + +/* ------------------------------------------------------------------------ */ +/* Teardown: drop promoted tables and remove source files */ +/* ------------------------------------------------------------------------ */ +proc casutil; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + droptable casdata="&tab2" incaslib="&testcaslib" quiet; + deletesource casdata="&tab1..sashdat" incaslib="&testcaslib" quiet; + deletesource casdata="src_&tab2..sashdat" incaslib="&testcaslib" quiet; +quit; + +cas mysess terminate; + +%let syscc=0; diff --git a/viya/mv_castabload.sas b/viya/mv_castabload.sas new file mode 100644 index 0000000..bc141fe --- /dev/null +++ b/viya/mv_castabload.sas @@ -0,0 +1,102 @@ +/** + @file mv_castabload.sas + @brief Checks if a CAS table exists in a CASLIB; if not, loads & promotes it + @details Runs in SPRE against an active CAS session. Uses + `table.tableExists` to check whether the table is already in-memory, + and PROC CASUTIL LOAD with the PROMOTE option to load it if not. + CASUTIL infers the file type from the source file extension. + + A CAS session must already be established by the caller, eg: + + cas mysess; + %mv_castabload(caslib=Public, table=BASEBALL) + + or (if not a hdat source with the same name as the table): + + %mv_castabload(caslib=Public, table=BASEBALL, + srcfile=MYBASEBALL.parquet) + + @param [in] caslib= CASLIB containing the source file + @param [in] table= Name to give the in-memory CAS table + @param [in] srcfile= (0) Source file name.ext in the caslib. If not provided, + the code assumes that srcfile=&table..hdat + @param [in] mdebug= (0) Set to 1 to enable verbose logging: + - echoes resolved parameters + - prints tableExists result + - enables mprint/notes during PROC calls + + @returns Sets global macro variable `MV_CASTABLOAD_RC`: + 0 = table already existed (no load performed) + 1 = table was loaded & promoted successfully + 3 = action failed +**/ + +%macro mv_castabload( + caslib= + ,table= + ,srcfile=0 + ,mdebug=0 +); + +%global MV_CASTABLOAD_RC; +%let MV_CASTABLOAD_RC=3; + +%local _sysopts; +%let _sysopts=%sysfunc(getoption(mprint)) %sysfunc(getoption(notes)); + +/* ---- input validation -------------------------------------------------- */ +%if "&caslib"="" or "&table"="" or "&srcfile"="" %then %do; + %put %str(ERR)OR: caslib=, table= and srcfile= are all required; + %return; +%end; +%if "&srcfile"="0" %then %let srcfile=&table..hdat; + +%if &mdebug=1 %then %do; + %put &=caslib; + %put &=table; + %put &=srcfile; + options mprint notes; +%end; + +/* ---- existence check --------------------------------------------------- */ +proc cas; + table.tableExists result=r / + caslib="&caslib" + name="&table"; + %if &mdebug=1 %then %do; + print r; + %end; + if r.exists = 0 then rc = 9; + else rc = 0; + symputx('MV_CASTABLOAD_RC', rc, 'G'); +quit; + + +/* ---- load if absent ---------------------------------------------------- */ +%if &MV_CASTABLOAD_RC=9 %then %do; + + proc casutil; + load casdata="&srcfile" + incaslib="&caslib" + casout="&table" + outcaslib="&caslib" + promote; + quit; + + %if &syserr=0 %then %let MV_CASTABLOAD_RC=1; + %else %let MV_CASTABLOAD_RC=3; + +%end; + +%if &MV_CASTABLOAD_RC=0 %then + %put NOTE: Table &caslib..&table already loaded - skipping; +%else %if &MV_CASTABLOAD_RC=1 %then + %put NOTE: Table &caslib..&table loaded and promoted; +%else %put ERROR: load failed for &caslib..&table; + +/* ---- restore options --------------------------------------------------- */ +%if &mdebug=1 %then %do; + options &_sysopts; +%end; + +%mend mv_castabload; From d0a5780cd18eb5d33d40478790a484099d7e71a0 Mon Sep 17 00:00:00 2001 From: 4gl <@> Date: Mon, 27 Apr 2026 17:29:07 +0100 Subject: [PATCH 02/13] feat: adding tests, adding param to mfv_existsashdat, updating README --- README.md | 2 +- base/mp_assert.sas | 1 + tests/viyaonly/mfv_existsashdat.test.sas | 99 ++++++++++++++++++++++++ tests/viyaonly/mv_castabload.test.sas | 2 +- viya/mfv_existsashdat.sas | 15 ++-- viya/mv_castabload.sas | 21 +++-- 6 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 tests/viyaonly/mfv_existsashdat.test.sas diff --git a/README.md b/README.md index ef1b40b..898c450 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ When contributing to this library, it is therefore important to ensure that all - All dataset references must be 2 level (eg `work.blah`, not `blah`). This is to avoid contention when options [DATASTMTCHK](https://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000279064.htm)=ALLKEYWORDS is in effect, or the [USER](https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/lrcon/n18m1vkqmeo4esn1moikt23zhp8s.htm) library is active. - Avoid naming collisions! All macro variables should be local scope. Use system generated work tables where possible - eg `data ; set sashelp.class; run; data &output; set &syslast; run;` - Where global macro variables are absolutely necessary, they should make use of `&sasjs_prefix` - see mp_init.sas -- The use of `quit;` for `proc sql` is optional unless you are looking to benefit from the timing statistics. +- The use of `quit;` for `proc sql` is essential, to avoid `WARNING: You cannot disconnect or terminate session XXXX until the procedure completes.` when terminating CAS sessions in Viya. - Use [sasjs lint](https://github.com/sasjs/lint)! ## General Notes diff --git a/base/mp_assert.sas b/base/mp_assert.sas index 86fdf56..e30e387 100644 --- a/base/mp_assert.sas +++ b/base/mp_assert.sas @@ -52,5 +52,6 @@ run; proc sql; drop table &ds; + quit; %mend mp_assert; \ No newline at end of file diff --git a/tests/viyaonly/mfv_existsashdat.test.sas b/tests/viyaonly/mfv_existsashdat.test.sas new file mode 100644 index 0000000..1dd2426 --- /dev/null +++ b/tests/viyaonly/mfv_existsashdat.test.sas @@ -0,0 +1,99 @@ +/** + @file + @brief Testing mfv_existsashdat macro function + +

SAS Macros

+ @li mf_uid.sas + @li mfv_existsashdat.sas + @li mp_assert.sas + @li mp_assertscope.sas + +**/ + +options mprint; + +/* ------------------------------------------------------------------------ */ +/* Setup: start a CAS session and stage a sashdat file in the Public caslib */ +/* ------------------------------------------------------------------------ */ +cas mysess; +caslib _all_ assign; + +%let testcaslib = Public; /* change this if Public isn't available */ +proc cas; + table.caslibInfo result=r / ; + found = 0; + do row over r.CASLibInfo; + if upcase(row.Name) = upcase("&testcaslib") then found = 1; + end; + if found = 0 then do; + print "ERROR: caslib &testcaslib not available"; + exit; + end; +quit; +%put NOTE: Using testcaslib=&testcaslib; + +%let tab1=T%mf_uid(); + +proc casutil; + load data=sashelp.baseball outcaslib="&testcaslib" casout="&tab1" replace; + save casdata="&tab1" incaslib="&testcaslib" + casout="&tab1..sashdat" outcaslib="&testcaslib" replace; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; +quit; + + +/* ------------------------------------------------------------------------ */ +%put TEST 1 - returns 1 when the sashdat file exists in the caslib; +/* ------------------------------------------------------------------------ */ +%mp_assert( + iftrue=(%mfv_existsashdat(&testcaslib..&tab1)=1), + desc=Test 1 - Check returns 1 for a sashdat that exists +) + +/* ------------------------------------------------------------------------ */ +%put TEST 2 - returns 0 when the file does not exist in the caslib; +/* ------------------------------------------------------------------------ */ +%mp_assertscope(SNAPSHOT) +%mp_assert( + iftrue=(%mfv_existsashdat(&testcaslib..DOESNOTEXIST_%mf_uid())=0), + desc=Check returns 0 for a sashdat that does not exist +) +%mp_assertscope(COMPARE, + desc=Check mfv_existsashdat does not leak macro variables into GLOBAL scope +) + +/* ------------------------------------------------------------------------ */ +%put TEST 3 - usecache= controls whether the cached dataset is reused; +/* ------------------------------------------------------------------------ */ + +/* First call: populates the cache dataset work.testcache_&testcaslib */ +%let _rc=%mfv_existsashdat(&testcaslib..&tab1,outprefix=work.testcache); + +/* Delete the sashdat from the caslib so a fresh scan would return 0 */ +proc casutil; + deletesource casdata="&tab1..sashdat" incaslib="&testcaslib" quiet; +quit; + +/* usecache=1 (default): must return 1 from the cached dataset */ +%mp_assert( + iftrue=(%mfv_existsashdat(&testcaslib..&tab1,outprefix=work.testcache)=1), + desc=Check returns 1 from cache even after source file is deleted +) + +/* usecache=0: forces rescan and reflects the deletion */ +%mp_assert( + iftrue=( + %mfv_existsashdat(&testcaslib..&tab1,usecache=0,outprefix=work.testcache)=0 + ), + desc=Check returns 0 when usecache=0 forces a rescan after source file deleted +) + +%let syscc=0; + + +/* ------------------------------------------------------------------------ */ +/* Teardown: terminate CAS session (sashdat already removed in TEST 4) */ +/* ------------------------------------------------------------------------ */ +cas mysess terminate; + +%let syscc=0; diff --git a/tests/viyaonly/mv_castabload.test.sas b/tests/viyaonly/mv_castabload.test.sas index d8dd200..8f5ca17 100644 --- a/tests/viyaonly/mv_castabload.test.sas +++ b/tests/viyaonly/mv_castabload.test.sas @@ -69,7 +69,7 @@ quit; /* ------------------------------------------------------------------------ */ -%put TEST 2 - load a table that does not yet exist (default srcfile=table.hdat); +%put TEST 2 - load a table that does not yet exist (default srcfile=table.sashdat); /* ------------------------------------------------------------------------ */ %mv_castabload(caslib=&testcaslib,table=&tab1,mdebug=1) diff --git a/viya/mfv_existsashdat.sas b/viya/mfv_existsashdat.sas index 5a256c9..84d47cd 100644 --- a/viya/mfv_existsashdat.sas +++ b/viya/mfv_existsashdat.sas @@ -6,14 +6,17 @@ %if %mfv_existsashdat(libds=casuser.sometable) %then %put yes it does!; The function uses `dosubl()` to run the `table.fileinfo` action, for the - specified library, filtering for `*.sashdat` tables. The results are stored - in a WORK table (&outprefix._&lib). If that table already exists, it is - queried instead, to avoid the dosubl() performance hit. + specified library, filtering for `*.sashdat` tables. + + IMPORTANT NOTE - The results are cached in a WORK table (&outprefix._&lib). + If that table already exists, it is queried instead, to avoid the + dosubl() performance hit. To force a rescan, just use a new `&outprefix` value, or delete the table(s) before running the function. @param [in] libds library.dataset + @param [in] usecache= (1) Set to 0 to rebuild the cache @param [out] outprefix= (work.mfv_existsashdat) Used to store current HDATA tables to improve subsequent query performance. This reference is a prefix and is converted to `&prefix._{libref}` @@ -24,14 +27,14 @@ @author Mathieu Blauw **/ -%macro mfv_existsashdat(libds,outprefix=work.mfv_existsashdat +%macro mfv_existsashdat(libds,usecache=1,outprefix=work.mfv_existsashdat ); %local rc dsid name lib ds; %let lib=%upcase(%scan(&libds,1,'.')); %let ds=%upcase(%scan(&libds,-1,'.')); /* if table does not exist, create it */ -%if %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; +%if &usecache ne 1 or %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; %let rc=%sysfunc(dosubl(%nrstr( /* Read in table list (once per &lib per session) */ proc cas; @@ -41,7 +44,7 @@ quit; /* Only keep name, without file extension */ data &outprefix._&lib; - set &outprefix._&lib(where=(Name like '%.sashdat') keep=Name); + set &outprefix._&lib(where=(upcase(Name) like '%.SASHDAT') keep=Name); Name=upcase(scan(Name,1,'.')); run; ))); diff --git a/viya/mv_castabload.sas b/viya/mv_castabload.sas index bc141fe..c7f4fd2 100644 --- a/viya/mv_castabload.sas +++ b/viya/mv_castabload.sas @@ -19,7 +19,7 @@ @param [in] caslib= CASLIB containing the source file @param [in] table= Name to give the in-memory CAS table @param [in] srcfile= (0) Source file name.ext in the caslib. If not provided, - the code assumes that srcfile=&table..hdat + the code assumes that srcfile=&table..sashdat @param [in] mdebug= (0) Set to 1 to enable verbose logging: - echoes resolved parameters - prints tableExists result @@ -28,7 +28,11 @@ @returns Sets global macro variable `MV_CASTABLOAD_RC`: 0 = table already existed (no load performed) 1 = table was loaded & promoted successfully - 3 = action failed + 3 = action failed (including source file missing) + +

SAS Macros

+ @li mfv_existsashdat.sas + **/ %macro mv_castabload( @@ -49,7 +53,7 @@ %put %str(ERR)OR: caslib=, table= and srcfile= are all required; %return; %end; -%if "&srcfile"="0" %then %let srcfile=&table..hdat; +%if "&srcfile"="0" %then %let srcfile=&table..sashdat; %if &mdebug=1 %then %do; %put &=caslib; @@ -58,6 +62,13 @@ options mprint notes; %end; +/* ---- check source file exists ------------------------------------------ */ +%if not %mfv_existsashdat(&caslib..%scan(&srcfile,1,.)) %then %do; + %put %str(ERR)OR: Source file "&srcfile" not found in caslib "&caslib"; + %let MV_CASTABLOAD_RC=3; + %return; +%end; + /* ---- existence check --------------------------------------------------- */ proc cas; table.tableExists result=r / @@ -92,11 +103,11 @@ quit; %put NOTE: Table &caslib..&table already loaded - skipping; %else %if &MV_CASTABLOAD_RC=1 %then %put NOTE: Table &caslib..&table loaded and promoted; -%else %put ERROR: load failed for &caslib..&table; +%else %put %str(ERR)OR: load failed for &caslib..&table; /* ---- restore options --------------------------------------------------- */ %if &mdebug=1 %then %do; options &_sysopts; %end; -%mend mv_castabload; +%mend mv_castabload; \ No newline at end of file From 7acaafae992a5b25482348160c9203d7feb886a4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 27 Apr 2026 16:29:53 +0000 Subject: [PATCH 03/13] chore: updating all.sas --- all.sas | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 122 insertions(+), 6 deletions(-) diff --git a/all.sas b/all.sas index 447822e..ae4a184 100644 --- a/all.sas +++ b/all.sas @@ -3512,6 +3512,7 @@ run; run; proc sql; drop table &ds; + quit; %mend mp_assert;/** @file @@ -24211,14 +24212,17 @@ run; %if %mfv_existsashdat(libds=casuser.sometable) %then %put yes it does!; The function uses `dosubl()` to run the `table.fileinfo` action, for the - specified library, filtering for `*.sashdat` tables. The results are stored - in a WORK table (&outprefix._&lib). If that table already exists, it is - queried instead, to avoid the dosubl() performance hit. + specified library, filtering for `*.sashdat` tables. + + IMPORTANT NOTE - The results are cached in a WORK table (&outprefix._&lib). + If that table already exists, it is queried instead, to avoid the + dosubl() performance hit. To force a rescan, just use a new `&outprefix` value, or delete the table(s) before running the function. @param [in] libds library.dataset + @param [in] usecache= (1) Set to 0 to rebuild the cache @param [out] outprefix= (work.mfv_existsashdat) Used to store current HDATA tables to improve subsequent query performance. This reference is a prefix and is converted to `&prefix._{libref}` @@ -24229,14 +24233,14 @@ run; @author Mathieu Blauw **/ -%macro mfv_existsashdat(libds,outprefix=work.mfv_existsashdat +%macro mfv_existsashdat(libds,usecache=1,outprefix=work.mfv_existsashdat ); %local rc dsid name lib ds; %let lib=%upcase(%scan(&libds,1,'.')); %let ds=%upcase(%scan(&libds,-1,'.')); /* if table does not exist, create it */ -%if %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; +%if &usecache ne 1 or %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; %let rc=%sysfunc(dosubl(%nrstr( /* Read in table list (once per &lib per session) */ proc cas; @@ -24246,7 +24250,7 @@ run; quit; /* Only keep name, without file extension */ data &outprefix._&lib; - set &outprefix._&lib(where=(Name like '%.sashdat') keep=Name); + set &outprefix._&lib(where=(upcase(Name) like '%.SASHDAT') keep=Name); Name=upcase(scan(Name,1,'.')); run; ))); @@ -24371,6 +24375,118 @@ run; msg=Cannot leave &sysmacroname with syscc=&syscc ) %mend mfv_getpathuri;/** + @file mv_castabload.sas + @brief Checks if a CAS table exists in a CASLIB; if not, loads & promotes it + @details Runs in SPRE against an active CAS session. Uses + `table.tableExists` to check whether the table is already in-memory, + and PROC CASUTIL LOAD with the PROMOTE option to load it if not. + CASUTIL infers the file type from the source file extension. + + A CAS session must already be established by the caller, eg: + + cas mysess; + %mv_castabload(caslib=Public, table=BASEBALL) + + or (if not a hdat source with the same name as the table): + + %mv_castabload(caslib=Public, table=BASEBALL, + srcfile=MYBASEBALL.parquet) + + @param [in] caslib= CASLIB containing the source file + @param [in] table= Name to give the in-memory CAS table + @param [in] srcfile= (0) Source file name.ext in the caslib. If not provided, + the code assumes that srcfile=&table..sashdat + @param [in] mdebug= (0) Set to 1 to enable verbose logging: + - echoes resolved parameters + - prints tableExists result + - enables mprint/notes during PROC calls + + @returns Sets global macro variable `MV_CASTABLOAD_RC`: + 0 = table already existed (no load performed) + 1 = table was loaded & promoted successfully + 3 = action failed (including source file missing) + +

SAS Macros

+ @li mfv_existsashdat.sas + +**/ + +%macro mv_castabload( + caslib= + ,table= + ,srcfile=0 + ,mdebug=0 +); + +%global MV_CASTABLOAD_RC; +%let MV_CASTABLOAD_RC=3; + +%local _sysopts; +%let _sysopts=%sysfunc(getoption(mprint)) %sysfunc(getoption(notes)); + +/* ---- input validation -------------------------------------------------- */ +%if "&caslib"="" or "&table"="" or "&srcfile"="" %then %do; + %put %str(ERR)OR: caslib=, table= and srcfile= are all required; + %return; +%end; +%if "&srcfile"="0" %then %let srcfile=&table..sashdat; + +%if &mdebug=1 %then %do; + %put &=caslib; + %put &=table; + %put &=srcfile; + options mprint notes; +%end; + +/* ---- check source file exists ------------------------------------------ */ +%if not %mfv_existsashdat(&caslib..%scan(&srcfile,1,.)) %then %do; + %put %str(ERR)OR: Source file "&srcfile" not found in caslib "&caslib"; + %let MV_CASTABLOAD_RC=3; + %return; +%end; + +/* ---- existence check --------------------------------------------------- */ +proc cas; + table.tableExists result=r / + caslib="&caslib" + name="&table"; + %if &mdebug=1 %then %do; + print r; + %end; + if r.exists = 0 then rc = 9; + else rc = 0; + symputx('MV_CASTABLOAD_RC', rc, 'G'); +quit; + + +/* ---- load if absent ---------------------------------------------------- */ +%if &MV_CASTABLOAD_RC=9 %then %do; + + proc casutil; + load casdata="&srcfile" + incaslib="&caslib" + casout="&table" + outcaslib="&caslib" + promote; + quit; + + %if &syserr=0 %then %let MV_CASTABLOAD_RC=1; + %else %let MV_CASTABLOAD_RC=3; + +%end; + +%if &MV_CASTABLOAD_RC=0 %then + %put NOTE: Table &caslib..&table already loaded - skipping; +%else %if &MV_CASTABLOAD_RC=1 %then + %put NOTE: Table &caslib..&table loaded and promoted; +%else %put %str(ERR)OR: load failed for &caslib..&table; + +/* ---- restore options --------------------------------------------------- */ +%if &mdebug=1 %then %do; + options &_sysopts; +%end; + +%mend mv_castabload;/** @file @brief Creates a file in SAS Drive using the API method @details Creates a file in SAS Drive using the API interface. From 73fd85d254ab5da7ed7d4378897c8c0fce04becd Mon Sep 17 00:00:00 2001 From: 4gl <@> Date: Tue, 28 Apr 2026 12:37:56 +0100 Subject: [PATCH 04/13] feat: new mfv_getcaslib macro Fetches a caslib from a regular SAS libref --- tests/viyaonly/mfv_getcaslib.test.sas | 69 +++++++++++++++++++++++++++ viya/mfv_getcaslib.sas | 37 ++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 tests/viyaonly/mfv_getcaslib.test.sas create mode 100644 viya/mfv_getcaslib.sas diff --git a/tests/viyaonly/mfv_getcaslib.test.sas b/tests/viyaonly/mfv_getcaslib.test.sas new file mode 100644 index 0000000..da70f0e --- /dev/null +++ b/tests/viyaonly/mfv_getcaslib.test.sas @@ -0,0 +1,69 @@ +/** + @file + @brief Testing mfv_getcaslib macro function + +

SAS Macros

+ @li mfv_getcaslib.sas + @li mp_assert.sas + @li mp_assertscope.sas + +**/ + +options mprint; + +/* ------------------------------------------------------------------------ */ +/* Setup: start a CAS session and assign caslibs */ +/* ------------------------------------------------------------------------ */ +cas mysess; +caslib _all_ assign; + +%let testcaslib=Public; + +libname castest cas caslib=&testcaslib; + +/* ------------------------------------------------------------------------ */ +%put TEST 1 - returns the caslib name for a valid CAS libref; +/* ------------------------------------------------------------------------ */ +%mp_assert( + iftrue=(%mfv_getcaslib(castest)=%upcase(&testcaslib)), + desc=Check correct caslib name returned for a valid CAS libref +) + + +/* ------------------------------------------------------------------------ */ +%put TEST 2 - returns empty for a non-CAS libref (WORK); +/* ------------------------------------------------------------------------ */ +%mp_assert( + iftrue=(%mfv_getcaslib(WORK)=), + desc=Check empty string returned for a non-CAS libref +) + + +/* ------------------------------------------------------------------------ */ +%put TEST 3 - returns empty for a libref that does not exist; +/* ------------------------------------------------------------------------ */ +%mp_assert( + iftrue=(%mfv_getcaslib(DOESNOTEXIST)=), + desc=Check empty string returned for a non-existent libref +) + + +/* ------------------------------------------------------------------------ */ +%put TEST 5 - no scope leakage into global macro variables; +/* ------------------------------------------------------------------------ */ +%mp_assertscope(SNAPSHOT) + +%let _rc=%mfv_getcaslib(castest); + +%mp_assertscope(COMPARE, + desc=Check mfv_getcaslib does not leak macro variables into GLOBAL scope, + ignorelist=_RC +) + + +/* ------------------------------------------------------------------------ */ +/* Teardown */ +/* ------------------------------------------------------------------------ */ +cas mysess terminate; + + diff --git a/viya/mfv_getcaslib.sas b/viya/mfv_getcaslib.sas new file mode 100644 index 0000000..e95693f --- /dev/null +++ b/viya/mfv_getcaslib.sas @@ -0,0 +1,37 @@ +/** + @file mfv_getcaslib.sas + @brief Returns the CAS caslib name for a given SAS libref + @details Pure macro function. Reads sashelp.vlibnam and returns + the sysvalue where sysname='Caslib' for the given libref. This + is useful when the caslib name and libref name may differ. + + Usage: + + %put %mfv_getcaslib(lib=PUBLIC); + + @param [in] lib SAS libref for which to return the CAS caslib name + + @return Returns the CAS caslib name, or empty string if not found + +**/ + +%macro mfv_getcaslib(lib); + +%local dsid rc result; + +%let dsid=%sysfunc(open(sashelp.vlibnam( + where=(libname="%upcase(&lib)" and sysname="Caslib") +))); + +%if &dsid %then %do; + %let rc=%sysfunc(fetch(&dsid)); + %if &rc=0 %then + %let result=%sysfunc( + getvarc(&dsid,%sysfunc(varnum(&dsid,SYSVALUE))) + ); + %let rc=%sysfunc(close(&dsid)); +%end; + +&result + +%mend mfv_getcaslib; From 7aa788e547c7614a8bc7656cb645d8c713c5b59a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Apr 2026 11:39:16 +0000 Subject: [PATCH 05/13] chore: updating all.sas --- all.sas | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/all.sas b/all.sas index ae4a184..13f4275 100644 --- a/all.sas +++ b/all.sas @@ -24267,6 +24267,43 @@ run; %else 0; %mend mfv_existsashdat; +/** + @file mfv_getcaslib.sas + @brief Returns the CAS caslib name for a given SAS libref + @details Pure macro function. Reads sashelp.vlibnam and returns + the sysvalue where sysname='Caslib' for the given libref. This + is useful when the caslib name and libref name may differ. + + Usage: + + %put %mfv_getcaslib(lib=PUBLIC); + + @param [in] lib SAS libref for which to return the CAS caslib name + + @return Returns the CAS caslib name, or empty string if not found + +**/ + +%macro mfv_getcaslib(lib); + +%local dsid rc result; + +%let dsid=%sysfunc(open(sashelp.vlibnam( + where=(libname="%upcase(&lib)" and sysname="Caslib") +))); + +%if &dsid %then %do; + %let rc=%sysfunc(fetch(&dsid)); + %if &rc=0 %then + %let result=%sysfunc( + getvarc(&dsid,%sysfunc(varnum(&dsid,SYSVALUE))) + ); + %let rc=%sysfunc(close(&dsid)); +%end; + +&result + +%mend mfv_getcaslib; /** @file @brief Returns the path of a folder from the URI From b6fad4a469bafadef093b46dde1008d19ac1ecf1 Mon Sep 17 00:00:00 2001 From: 4gl <@> Date: Tue, 28 Apr 2026 12:39:27 +0100 Subject: [PATCH 06/13] chore: adding quit in mp_assertscope and excluding .claude in .gitignore --- .gitignore | 1 + base/mp_assertscope.sas | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 288005b..e958365 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ mc_* ~ +.claude \ No newline at end of file diff --git a/base/mp_assertscope.sas b/base/mp_assertscope.sas index 213b146..5e24c93 100644 --- a/base/mp_assertscope.sas +++ b/base/mp_assertscope.sas @@ -92,6 +92,7 @@ from dictionary.macros where scope="&scope" and upcase(name) not in (%mf_getquotedstr(&ilist)) order by name,offset; + quit; %end; %else %if &action=COMPARE %then %do; @@ -129,7 +130,6 @@ %let test_comments=%str(Mod:(&mod) Add:(&add) Del:(&del)); %end; - data ; length test_description $256 test_result $4 test_comments $256; test_description=symget('desc'); @@ -142,6 +142,7 @@ run; proc sql; drop table &ds; + quit; %end; %mend mp_assertscope; From 8a22280627fbe35f3cec420983147f85cd62cf6b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Apr 2026 11:40:09 +0000 Subject: [PATCH 07/13] chore: updating all.sas --- all.sas | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/all.sas b/all.sas index 13f4275..82e9cd8 100644 --- a/all.sas +++ b/all.sas @@ -4043,6 +4043,7 @@ run; from dictionary.macros where scope="&scope" and upcase(name) not in (%mf_getquotedstr(&ilist)) order by name,offset; + quit; %end; %else %if &action=COMPARE %then %do; @@ -4080,7 +4081,6 @@ run; %let test_comments=%str(Mod:(&mod) Add:(&add) Del:(&del)); %end; - data ; length test_description $256 test_result $4 test_comments $256; test_description=symget('desc'); @@ -4093,6 +4093,7 @@ run; run; proc sql; drop table &ds; + quit; %end; %mend mp_assertscope; From 7ef58a0f540905cf9466fb4a8b80b4fafc512588 Mon Sep 17 00:00:00 2001 From: 4gl <@> Date: Tue, 28 Apr 2026 17:36:12 +0100 Subject: [PATCH 08/13] feat: mv_castabsave macro and tests --- base/mp_init.sas | 3 +- tests/viyaonly/mv_castabsave.test.sas | 158 +++++++++++++++++++++ viya/mv_castabsave.sas | 192 ++++++++++++++++++++++++++ 3 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 tests/viyaonly/mv_castabsave.test.sas create mode 100644 viya/mv_castabsave.sas diff --git a/base/mp_init.sas b/base/mp_init.sas index ee2231b..68b4e19 100644 --- a/base/mp_init.sas +++ b/base/mp_init.sas @@ -41,8 +41,9 @@ &prefix._INIT_NUM /* initialisation time as numeric */ &prefix._INIT_DTTM /* initialisation time in E8601DT26.6 format */ &prefix.WORK /* avoid typing %sysfunc(pathname(work)) every time */ + &prefix.PROCESSMODE + &prefix._STPSRV_HEADER_LOC ; - %let sasjs_prefix=&prefix; data _null_; diff --git a/tests/viyaonly/mv_castabsave.test.sas b/tests/viyaonly/mv_castabsave.test.sas new file mode 100644 index 0000000..20bb0f6 --- /dev/null +++ b/tests/viyaonly/mv_castabsave.test.sas @@ -0,0 +1,158 @@ +/** + @file + @brief Testing mv_castabsave macro + +

SAS Macros

+ @li mf_uid.sas + @li mp_assert.sas + @li mp_assertscope.sas + @li mv_castabsave.sas + +**/ + +options mprint; + +/* -------------------------------------------------------------------- */ +/* Setup: start a CAS session and load a table that has a tracked */ +/* source file so mv_castabsave can discover it via the REST API */ +/* -------------------------------------------------------------------- */ +cas mysess; +caslib _all_ assign; + +%let testcaslib=Public; + +proc cas; + table.caslibInfo result=r / ; + found=0; + do row over r.CASLibInfo; + if upcase(row.Name)=upcase("&testcaslib") then found=1; + end; + if found=0 then do; + print "ERROR: caslib &testcaslib not available"; + exit; + end; +quit; +%put NOTE: Using testcaslib=&testcaslib; + +%let tab1=T%mf_uid(); + +/* Load sashelp.class into CAS, save as sashdat, reload from that file + so the table has a tracked source path (needed for REST discovery) */ +proc casutil; + load data=sashelp.class + outcaslib="&testcaslib" casout="&tab1" replace; + save casdata="&tab1" incaslib="&testcaslib" + casout="&tab1..sashdat" outcaslib="&testcaslib" replace; + /* Drop any existing global-scope version before promoting */ + /* runs twice (with quiet) as first would drop local scope if exists */ + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + + load casdata="&tab1..sashdat" incaslib="&testcaslib" + casout="&tab1" outcaslib="&testcaslib" promote; +quit; + +libname mylib cas caslib="&testcaslib"; + + +/* -------------------------------------------------------------------- */ +%put TEST 1 - save in-memory table back to disk + no scope leakage; +/* -------------------------------------------------------------------- */ + +/* Source file is removed so that the reload proves mv_castabsave + created the file from scratch, not that a prior version existed */ +proc casutil; + deletesource casdata="&tab1..sashdat" + incaslib="&testcaslib" quiet; +quit; + +/* Insert a sentinel row - it must survive the full save/drop/reload */ +data work.appendme; + set mylib.&tab1; + name='TESTROW'; + output; + stop; +proc casutil; + load data=work.appendme casout="&tab1" outcaslib="&testcaslib" append; +quit; + +%mp_assertscope(SNAPSHOT) + +%mv_castabsave(lib=mylib, table=&tab1, mdebug=1) + +%mp_assertscope(COMPARE, + desc=Check mv_castabsave does not leak macro variables into GLOBAL scope, + ignorelist=MC0_JADP1LEN MC0_JADP2LEN MC0_JADP3LEN MC0_JADPNUM MC0_JADVLEN +) + +proc casutil; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + load casdata="&tab1..sashdat" incaslib="&testcaslib" + casout="&tab1" outcaslib="&testcaslib" promote; +quit; + +%let _rowcount=0; +proc sql noprint; + select count(*) into :_rowcount + from mylib.&tab1 + where name='TESTROW'; +quit; + +%mp_assert( + iftrue=(&_rowcount=1), + desc=Check inserted row survives mv_castabsave round-trip to disk +) + + +/* -------------------------------------------------------------------- */ +%put TEST 2 - save overwrites an existing source file; +/* -------------------------------------------------------------------- */ + +/* Source file already exists from the TEST 1 save - append a new row */ +data work.appendme; + set mylib.&tab1; + name='TESTROW2'; + output; + stop; +proc casutil; + load data=work.appendme casout="&tab1" + outcaslib="&testcaslib" append; +quit; + +%mv_castabsave(lib=mylib, table=&tab1, mdebug=1) + +proc casutil; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + load casdata="&tab1..sashdat" incaslib="&testcaslib" + casout="&tab1" outcaslib="&testcaslib" promote; +quit; + +%let _rowcount=0; +proc sql noprint; + select count(*) into :_rowcount + from mylib.&tab1 + where name='TESTROW2'; +quit; + +%mp_assert( + iftrue=(&_rowcount=1), + desc=Check inserted row survives save over an existing source file +) + + +/* -------------------------------------------------------------------- */ +/* Teardown */ +/* -------------------------------------------------------------------- */ +libname mylib clear; + +proc casutil; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + deletesource casdata="&tab1..sashdat" + incaslib="&testcaslib" quiet; +quit; + +cas mysess terminate; + +%let syscc=0; diff --git a/viya/mv_castabsave.sas b/viya/mv_castabsave.sas new file mode 100644 index 0000000..87d6aa1 --- /dev/null +++ b/viya/mv_castabsave.sas @@ -0,0 +1,192 @@ +/** + @file mv_castabsave.sas + @brief Saves an in-memory CAS table back to persistent storage + @details Runs in SPRE against an active CAS session. Accepts a + SAS libref, derives the CAS caslib and session UUID from + sashelp.vlibnam, locates the owning CAS server via the + casManagement REST API, then queries the table endpoint to + discover the original source file and saves back to that path. + CASUTIL infers the file type from the output file extension. + + A CAS session must already be established by the caller, eg: + + cas mysess; + libname mylib cas caslib=Public; + %mv_castabsave(lib=mylib, table=BASEBALL) + + @param [in] lib= SAS libref for the CAS caslib + @param [in] table= Name of the in-memory CAS table to save + @param [in] mdebug= (0) Set to 1 to enable verbose logging: + - echoes resolved parameters + - prints HTTP response body + - enables mprint/notes during PROC calls + +

SAS Macros

+ @li mf_getplatform.sas + @li mf_getuniquefileref.sas + @li mf_getuniquelibref.sas + @li mp_abort.sas + +**/ + +%macro mv_castabsave( + lib= + ,table= + ,mdebug=0 +); + +%local _sysopts base_uri caslib uuid server + srcfile srccaslib fname1 libref1 ftmp i _svcount; +%let _sysopts=%sysfunc(getoption(mprint)) %sysfunc(getoption(notes)); + +/* ---- input validation -------------------------------------------------- */ +%mp_abort( + iftrue=("&lib"="" or "&table"=""), + msg=%str(lib= and table= are required) +) + +%if &mdebug=1 %then %do; + %put &=lib; + %put &=table; + options mprint notes; +%end; + +/* ---- derive caslib and session UUID from sashelp.vlibnam --------------- */ +data _null_; + set sashelp.vlibnam( + where=(libname="%upcase(&lib)" + and sysname in ("Caslib","Session UUID")) + ); + if sysname="Caslib" then call symputx('caslib',sysvalue,'L'); + else call symputx('uuid',sysvalue,'L'); +run; + +%mp_abort( + iftrue=("&caslib"=""), + msg=%str(&lib is not an assigned CAS libref) +) + +%mp_abort( + iftrue=("&uuid"=""), + msg=%str(No session UUID found for libref &lib) +) + +%if &mdebug=1 %then %do; + %put &=caslib; + %put &=uuid; +%end; + +%let base_uri=%mf_getplatform(VIYARESTAPI); + +/* ---- get list of CAS servers ------------------------------------------- */ +%let fname1=%mf_getuniquefileref(); +%let libref1=%mf_getuniquelibref(); + +proc http method='GET' out=&fname1 oauth_bearer=sas_services + url="&base_uri/casManagement/servers"; +run; + +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200), + msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) +) + +libname &libref1 JSON fileref=&fname1; + +data _null_; + set &libref1..items; + call symputx(cats('_sv_', _n_), name, 'L'); + call symputx('_svcount', _n_, 'L'); +run; + +libname &libref1 clear; +filename &fname1 clear; + +/* ---- find which server owns this session ------------------------------- */ +%do i=1 %to &_svcount; + %if "&server"="" %then %do; + %if &mdebug=1 %then %put checking server: &&_sv_&i; + %let ftmp=%mf_getuniquefileref(); + proc http method='GET' out=&ftmp oauth_bearer=sas_services + url="&base_uri/casManagement/servers/&&_sv_&i/sessions/&uuid"; + run; + %if &SYS_PROCHTTP_STATUS_CODE=200 + %then %let server=&&_sv_&i; + filename &ftmp clear; + %end; +%end; + +%mp_abort( + iftrue=("&server"=""), + msg=%str(Could not find owning server for CAS session &uuid) +) + +%if &mdebug=1 %then %put &=server; + +/* ---- discover srcfile from REST endpoint ------------------------------- */ +%let fname1=%mf_getuniquefileref(); +%let libref1=%mf_getuniquelibref(); + +proc http method='GET' out=&fname1 oauth_bearer=sas_services + url="&base_uri/casManagement/servers/&server/caslibs/&caslib/tables/&table"; +run; + +%if &mdebug=1 %then %do; + %put &=SYS_PROCHTTP_STATUS_CODE &=SYS_PROCHTTP_STATUS_PHRASE; + data _null_; + infile &fname1; + input; + putlog _infile_; + run; +%end; + +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE=404), + msg=%str(&caslib..&table not found - is it loaded in memory?) +) +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200), + msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) +) + +libname &libref1 JSON fileref=&fname1; + +data _null_; + set &libref1..tablereference; + call symputx('srcfile', sourceTableName, 'L'); + call symputx('srccaslib', sourceCaslibName, 'L'); + stop; +run; + +libname &libref1 clear; +filename &fname1 clear; + +%mp_abort( + iftrue=("&srcfile"="" or "&srccaslib"=""), + msg=%str(No sourceTableName/sourceCaslibName for &caslib..&table) +) + +%if &mdebug=1 %then %put &=srcfile; + +/* ---- save to disk ------------------------------------------------------- */ +proc casutil; + save casdata="&table" + incaslib="&caslib" + casout="&srcfile" + outcaslib="&srccaslib" + replace; +quit; + +%mp_abort( + iftrue=(&syscc ne 0), + msg=%str(Save failed for &caslib..&table) +) + +%put NOTE: Table &caslib..&table saved to &srcfile; + +/* ---- restore options --------------------------------------------------- */ +%if &mdebug=1 %then %do; + options &_sysopts; +%end; + +%mend mv_castabsave; From 1c005586dc73da84d4f6b3798ee657567ba97895 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Apr 2026 16:36:58 +0000 Subject: [PATCH 09/13] chore: updating all.sas --- all.sas | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/all.sas b/all.sas index 82e9cd8..3d00f66 100644 --- a/all.sas +++ b/all.sas @@ -10105,8 +10105,9 @@ filename &tempref clear; &prefix._INIT_NUM /* initialisation time as numeric */ &prefix._INIT_DTTM /* initialisation time in E8601DT26.6 format */ &prefix.WORK /* avoid typing %sysfunc(pathname(work)) every time */ + &prefix.PROCESSMODE + &prefix._STPSRV_HEADER_LOC ; - %let sasjs_prefix=&prefix; data _null_; @@ -24525,6 +24526,198 @@ quit; %end; %mend mv_castabload;/** + @file mv_castabsave.sas + @brief Saves an in-memory CAS table back to persistent storage + @details Runs in SPRE against an active CAS session. Accepts a + SAS libref, derives the CAS caslib and session UUID from + sashelp.vlibnam, locates the owning CAS server via the + casManagement REST API, then queries the table endpoint to + discover the original source file and saves back to that path. + CASUTIL infers the file type from the output file extension. + + A CAS session must already be established by the caller, eg: + + cas mysess; + libname mylib cas caslib=Public; + %mv_castabsave(lib=mylib, table=BASEBALL) + + @param [in] lib= SAS libref for the CAS caslib + @param [in] table= Name of the in-memory CAS table to save + @param [in] mdebug= (0) Set to 1 to enable verbose logging: + - echoes resolved parameters + - prints HTTP response body + - enables mprint/notes during PROC calls + +

SAS Macros

+ @li mf_getplatform.sas + @li mf_getuniquefileref.sas + @li mf_getuniquelibref.sas + @li mp_abort.sas + +**/ + +%macro mv_castabsave( + lib= + ,table= + ,mdebug=0 +); + +%local _sysopts base_uri caslib uuid server + srcfile srccaslib fname1 libref1 ftmp i _svcount; +%let _sysopts=%sysfunc(getoption(mprint)) %sysfunc(getoption(notes)); + +/* ---- input validation -------------------------------------------------- */ +%mp_abort( + iftrue=("&lib"="" or "&table"=""), + msg=%str(lib= and table= are required) +) + +%if &mdebug=1 %then %do; + %put &=lib; + %put &=table; + options mprint notes; +%end; + +/* ---- derive caslib and session UUID from sashelp.vlibnam --------------- */ +data _null_; + set sashelp.vlibnam( + where=(libname="%upcase(&lib)" + and sysname in ("Caslib","Session UUID")) + ); + if sysname="Caslib" then call symputx('caslib',sysvalue,'L'); + else call symputx('uuid',sysvalue,'L'); +run; + +%mp_abort( + iftrue=("&caslib"=""), + msg=%str(&lib is not an assigned CAS libref) +) + +%mp_abort( + iftrue=("&uuid"=""), + msg=%str(No session UUID found for libref &lib) +) + +%if &mdebug=1 %then %do; + %put &=caslib; + %put &=uuid; +%end; + +%let base_uri=%mf_getplatform(VIYARESTAPI); + +/* ---- get list of CAS servers ------------------------------------------- */ +%let fname1=%mf_getuniquefileref(); +%let libref1=%mf_getuniquelibref(); + +proc http method='GET' out=&fname1 oauth_bearer=sas_services + url="&base_uri/casManagement/servers"; +run; + +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200), + msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) +) + +libname &libref1 JSON fileref=&fname1; + +data _null_; + set &libref1..items; + call symputx(cats('_sv_', _n_), name, 'L'); + call symputx('_svcount', _n_, 'L'); +run; + +libname &libref1 clear; +filename &fname1 clear; + +/* ---- find which server owns this session ------------------------------- */ +%do i=1 %to &_svcount; + %if "&server"="" %then %do; + %if &mdebug=1 %then %put checking server: &&_sv_&i; + %let ftmp=%mf_getuniquefileref(); + proc http method='GET' out=&ftmp oauth_bearer=sas_services + url="&base_uri/casManagement/servers/&&_sv_&i/sessions/&uuid"; + run; + %if &SYS_PROCHTTP_STATUS_CODE=200 + %then %let server=&&_sv_&i; + filename &ftmp clear; + %end; +%end; + +%mp_abort( + iftrue=("&server"=""), + msg=%str(Could not find owning server for CAS session &uuid) +) + +%if &mdebug=1 %then %put &=server; + +/* ---- discover srcfile from REST endpoint ------------------------------- */ +%let fname1=%mf_getuniquefileref(); +%let libref1=%mf_getuniquelibref(); + +proc http method='GET' out=&fname1 oauth_bearer=sas_services + url="&base_uri/casManagement/servers/&server/caslibs/&caslib/tables/&table"; +run; + +%if &mdebug=1 %then %do; + %put &=SYS_PROCHTTP_STATUS_CODE &=SYS_PROCHTTP_STATUS_PHRASE; + data _null_; + infile &fname1; + input; + putlog _infile_; + run; +%end; + +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE=404), + msg=%str(&caslib..&table not found - is it loaded in memory?) +) +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200), + msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) +) + +libname &libref1 JSON fileref=&fname1; + +data _null_; + set &libref1..tablereference; + call symputx('srcfile', sourceTableName, 'L'); + call symputx('srccaslib', sourceCaslibName, 'L'); + stop; +run; + +libname &libref1 clear; +filename &fname1 clear; + +%mp_abort( + iftrue=("&srcfile"="" or "&srccaslib"=""), + msg=%str(No sourceTableName/sourceCaslibName for &caslib..&table) +) + +%if &mdebug=1 %then %put &=srcfile; + +/* ---- save to disk ------------------------------------------------------- */ +proc casutil; + save casdata="&table" + incaslib="&caslib" + casout="&srcfile" + outcaslib="&srccaslib" + replace; +quit; + +%mp_abort( + iftrue=(&syscc ne 0), + msg=%str(Save failed for &caslib..&table) +) + +%put NOTE: Table &caslib..&table saved to &srcfile; + +/* ---- restore options --------------------------------------------------- */ +%if &mdebug=1 %then %do; + options &_sysopts; +%end; + +%mend mv_castabsave; +/** @file @brief Creates a file in SAS Drive using the API method @details Creates a file in SAS Drive using the API interface. From 36452a2a02b5ad0259f6d74b4710de7edd6a008d Mon Sep 17 00:00:00 2001 From: 4gl <@> Date: Tue, 28 Apr 2026 18:38:57 +0100 Subject: [PATCH 10/13] fix: simplifying mv_castabload and improving tests --- tests/viyaonly/mv_castabload.test.sas | 192 +++++++++++----------- viya/mv_castabload.sas | 224 ++++++++++++++++++-------- 2 files changed, 257 insertions(+), 159 deletions(-) diff --git a/tests/viyaonly/mv_castabload.test.sas b/tests/viyaonly/mv_castabload.test.sas index 8f5ca17..6c6d3f9 100644 --- a/tests/viyaonly/mv_castabload.test.sas +++ b/tests/viyaonly/mv_castabload.test.sas @@ -7,26 +7,26 @@ @li mp_assert.sas @li mp_assertscope.sas @li mv_castabload.sas - @li mv_createfile.sas **/ options mprint; -/* ------------------------------------------------------------------------ */ -/* Setup: start a CAS session and stage a source file in the Public caslib */ -/* ------------------------------------------------------------------------ */ +/* -------------------------------------------------------------------- */ +/* Setup: start a CAS session and stage a source file in the caslib */ +/* -------------------------------------------------------------------- */ cas mysess; caslib _all_ assign; -%let testcaslib = Public; /* change this if Public isn't available */ +%let testcaslib=Public; + proc cas; table.caslibInfo result=r / ; - found = 0; + found=0; do row over r.CASLibInfo; - if upcase(row.Name) = upcase("&testcaslib") then found = 1; + if upcase(row.Name)=upcase("&testcaslib") then found=1; end; - if found = 0 then do; + if found=0 then do; print "ERROR: caslib &testcaslib not available"; exit; end; @@ -34,113 +34,117 @@ quit; %put NOTE: Using testcaslib=&testcaslib; %let tab1=T%mf_uid(); -%let tab2=T%mf_uid(); -%let tab3=T%mf_uid(); -/* Create a SASHDAT source file in the Public caslib from SASHELP.BASEBALL - so that subsequent LOAD operations have something real to pick up. */ +/* Save a sashdat source file then drop the in-memory copy so the first + mv_castabload call has something to load */ proc casutil; - load data=sashelp.baseball outcaslib="&testcaslib" casout="&tab1" replace; + load data=sashelp.baseball + outcaslib="&testcaslib" casout="&tab1" replace; save casdata="&tab1" incaslib="&testcaslib" casout="&tab1..sashdat" outcaslib="&testcaslib" replace; droptable casdata="&tab1" incaslib="&testcaslib" quiet; quit; -/* And a second hdat, with a name different from the table name, so that we - can exercise the explicit srcfile= path. */ -proc casutil; - load data=sashelp.cars outcaslib="&testcaslib" casout="&tab2" replace; - save casdata="&tab2" incaslib="&testcaslib" - casout="src_&tab2..sashdat" outcaslib="&testcaslib" replace; - droptable casdata="&tab2" incaslib="&testcaslib" quiet; +libname mylib cas caslib="&testcaslib"; + + +/* -------------------------------------------------------------------- */ +%put TEST 1 - load a table that is not in memory; +/* -------------------------------------------------------------------- */ + +/* Confirm table is absent before the call */ +%let _tabexists=0; +proc cas; + table.tableExists result=r / + caslib="&testcaslib" name="&tab1"; + if r.exists > 0 then call symputx('_tabexists','1'); quit; +%mp_assert( + iftrue=(&_tabexists=0), + desc=Check table is not in memory before mv_castabload +) -/* ------------------------------------------------------------------------ */ -%put TEST 1 - missing required parameters returns without setting RC to 0/1; -/* ------------------------------------------------------------------------ */ -%let MV_CASTABLOAD_RC=; -%mv_castabload(caslib=,table=,srcfile=) +%mv_castabload(lib=mylib, table=&tab1, mdebug=1) + +%let _tabexists=0; +proc cas; + table.tableExists result=r / + caslib="&testcaslib" name="&tab1"; + if r.exists > 0 then call symputx('_tabexists','1'); +quit; %mp_assert( - iftrue=(&MV_CASTABLOAD_RC=3), - desc=Check RC=3 (initial/failure value) when required params are missing + iftrue=(&_tabexists=1), + desc=Check table is in memory after mv_castabload ) -/* ------------------------------------------------------------------------ */ -%put TEST 2 - load a table that does not yet exist (default srcfile=table.sashdat); -/* ------------------------------------------------------------------------ */ -%mv_castabload(caslib=&testcaslib,table=&tab1,mdebug=1) +/* -------------------------------------------------------------------- */ +%put TEST 2 - reload fetches a fresh copy and discards in-memory changes; +/* -------------------------------------------------------------------- */ + +/* Append a sentinel row to the in-memory table */ +data work.extra; + set mylib.&tab1; + name='TESTROW'; + output; + stop; +run; +proc casutil; + load data=work.extra casout="&tab1" + outcaslib="&testcaslib" append; +quit; + +%let _modified=0; +proc sql noprint; + select count(*) into :_modified + from mylib.&tab1 + where name='TESTROW'; +quit; %mp_assert( - iftrue=(&MV_CASTABLOAD_RC=1), - desc=Check RC=1 when table is loaded and promoted for the first time + iftrue=(&_modified=1), + desc=Check sentinel row is present in memory before reload ) - -/* ------------------------------------------------------------------------ */ -%put TEST 3 - calling again for the same table should be a no-op (RC=0); -/* also verify no scope leakage of macro variables */ -/* ------------------------------------------------------------------------ */ -%mp_assertscope(SNAPSHOT) - -%mv_castabload(caslib=&testcaslib,table=&tab1,mdebug=1) - -%mp_assertscope(COMPARE, - desc=Check mv_castabload does not leak macro variables into GLOBAL scope, - ignorelist=MV_CASTABLOAD_RC -) - -%mp_assert( - iftrue=(&MV_CASTABLOAD_RC=0), - desc=Check RC=0 when table is already in-memory (skip load) -) - - -/* ------------------------------------------------------------------------ */ -%put TEST 4 - explicit srcfile= where file name differs from table name; -/* ------------------------------------------------------------------------ */ -%mv_castabload( - caslib=&testcaslib, - table=&tab2, - srcfile=src_&tab2..sashdat, - mdebug=1 -) - -%mp_assert( - iftrue=(&MV_CASTABLOAD_RC=1), - desc=Check RC=1 when loading with explicit srcfile= parameter -) - - -/* ------------------------------------------------------------------------ */ -%put TEST 5 - load failure when srcfile does not exist in the caslib; -/* ------------------------------------------------------------------------ */ -%mv_castabload( - caslib=&testcaslib, - table=&tab3, - srcfile=doesnotexist_%mf_uid..sashdat, - mdebug=1 -) - -%mp_assert( - iftrue=(&MV_CASTABLOAD_RC=3), - desc=Check RC=3 when source file cannot be found / load fails -) - -/* reset so that a downstream failure RC does not break testterm */ -%let syscc=0; - - -/* ------------------------------------------------------------------------ */ -/* Teardown: drop promoted tables and remove source files */ -/* ------------------------------------------------------------------------ */ +/* Drop the table and reload - source file does not have the sentinel */ proc casutil; droptable casdata="&tab1" incaslib="&testcaslib" quiet; - droptable casdata="&tab2" incaslib="&testcaslib" quiet; - deletesource casdata="&tab1..sashdat" incaslib="&testcaslib" quiet; - deletesource casdata="src_&tab2..sashdat" incaslib="&testcaslib" quiet; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; +quit; + +%mp_assertscope(SNAPSHOT) + +%mv_castabload(lib=mylib, table=&tab1, mdebug=1) + +%mp_assertscope(COMPARE, + desc=Check mv_castabload does not leak macro variables into GLOBAL scope +) + +%let _after=0; +proc sql noprint; + select count(*) into :_after + from mylib.&tab1 + where name='TESTROW'; +quit; + +%mp_assert( + iftrue=(&_after=0), + desc=Check sentinel row is absent after reload from source +) + + +/* -------------------------------------------------------------------- */ +/* Teardown */ +/* -------------------------------------------------------------------- */ +libname mylib clear; + +proc casutil; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + droptable casdata="&tab1" incaslib="&testcaslib" quiet; + deletesource casdata="&tab1..sashdat" + incaslib="&testcaslib" quiet; quit; cas mysess terminate; diff --git a/viya/mv_castabload.sas b/viya/mv_castabload.sas index c7f4fd2..4bdb13a 100644 --- a/viya/mv_castabload.sas +++ b/viya/mv_castabload.sas @@ -1,73 +1,78 @@ /** @file mv_castabload.sas - @brief Checks if a CAS table exists in a CASLIB; if not, loads & promotes it - @details Runs in SPRE against an active CAS session. Uses - `table.tableExists` to check whether the table is already in-memory, - and PROC CASUTIL LOAD with the PROMOTE option to load it if not. - CASUTIL infers the file type from the source file extension. + @brief Checks if a CAS table is loaded; if not, loads and promotes it + @details Runs in SPRE against an active CAS session. Accepts a + SAS libref, derives the CAS caslib and session UUID from + sashelp.vlibnam, then checks whether the table is already + in-memory. If not, locates the owning CAS server via the + casManagement REST API, queries the table endpoint to discover + the source file and caslib, then loads and promotes the table. A CAS session must already be established by the caller, eg: cas mysess; - %mv_castabload(caslib=Public, table=BASEBALL) + libname mylib cas caslib=Public; + %mv_castabload(lib=mylib, table=BASEBALL) - or (if not a hdat source with the same name as the table): - - %mv_castabload(caslib=Public, table=BASEBALL, - srcfile=MYBASEBALL.parquet) - - @param [in] caslib= CASLIB containing the source file - @param [in] table= Name to give the in-memory CAS table - @param [in] srcfile= (0) Source file name.ext in the caslib. If not provided, - the code assumes that srcfile=&table..sashdat - @param [in] mdebug= (0) Set to 1 to enable verbose logging: + @param [in] lib= SAS libref for the CAS caslib + @param [in] table= Name of the CAS table to load + @param [in] mdebug= (0) Set to 1 to enable verbose logging: - echoes resolved parameters - prints tableExists result - enables mprint/notes during PROC calls - @returns Sets global macro variable `MV_CASTABLOAD_RC`: - 0 = table already existed (no load performed) - 1 = table was loaded & promoted successfully - 3 = action failed (including source file missing) -

SAS Macros

- @li mfv_existsashdat.sas + @li mf_getplatform.sas + @li mf_getuniquefileref.sas + @li mf_getuniquelibref.sas + @li mp_abort.sas **/ %macro mv_castabload( - caslib= + lib= ,table= - ,srcfile=0 ,mdebug=0 ); -%global MV_CASTABLOAD_RC; -%let MV_CASTABLOAD_RC=3; - -%local _sysopts; +%local _sysopts base_uri caslib uuid server + srcfile srccaslib fname1 libref1 ftmp i _svcount _exists; %let _sysopts=%sysfunc(getoption(mprint)) %sysfunc(getoption(notes)); /* ---- input validation -------------------------------------------------- */ -%if "&caslib"="" or "&table"="" or "&srcfile"="" %then %do; - %put %str(ERR)OR: caslib=, table= and srcfile= are all required; - %return; -%end; -%if "&srcfile"="0" %then %let srcfile=&table..sashdat; +%mp_abort( + iftrue=("&lib"="" or "&table"=""), + msg=%str(lib= and table= are required) +) %if &mdebug=1 %then %do; - %put &=caslib; + %put &=lib; %put &=table; - %put &=srcfile; options mprint notes; %end; -/* ---- check source file exists ------------------------------------------ */ -%if not %mfv_existsashdat(&caslib..%scan(&srcfile,1,.)) %then %do; - %put %str(ERR)OR: Source file "&srcfile" not found in caslib "&caslib"; - %let MV_CASTABLOAD_RC=3; - %return; -%end; +/* ---- derive caslib and session UUID from sashelp.vlibnam --------------- */ +data _null_; + set sashelp.vlibnam( + where=(libname="%upcase(&lib)" + and sysname in ("Caslib","Session UUID")) + ); + if sysname="Caslib" then call symputx('caslib',sysvalue,'L'); + else call symputx('uuid',sysvalue,'L'); + %if &mdebug=1 %then %do; + putlog sysname sysvalue; + %end; +run; + +%mp_abort( + iftrue=("&caslib"=""), + msg=%str(&lib is not an assigned CAS libref) +) + +%mp_abort( + iftrue=("&uuid"=""), + msg=%str(No session UUID found for libref &lib) +) /* ---- existence check --------------------------------------------------- */ proc cas; @@ -77,37 +82,126 @@ proc cas; %if &mdebug=1 %then %do; print r; %end; - if r.exists = 0 then rc = 9; - else rc = 0; - symputx('MV_CASTABLOAD_RC', rc, 'G'); + if r.exists > 0 then call symputx('_exists', '1', 'L'); + else call symputx('_exists', '0', 'L'); quit; - -/* ---- load if absent ---------------------------------------------------- */ -%if &MV_CASTABLOAD_RC=9 %then %do; - - proc casutil; - load casdata="&srcfile" - incaslib="&caslib" - casout="&table" - outcaslib="&caslib" - promote; - quit; - - %if &syserr=0 %then %let MV_CASTABLOAD_RC=1; - %else %let MV_CASTABLOAD_RC=3; - +/* ---- already loaded: skip ---------------------------------------------- */ +%if &_exists=1 %then %do; + %put NOTE: Table &caslib..&table already loaded - skipping; + %return; %end; -%if &MV_CASTABLOAD_RC=0 %then - %put NOTE: Table &caslib..&table already loaded - skipping; -%else %if &MV_CASTABLOAD_RC=1 %then - %put NOTE: Table &caslib..&table loaded and promoted; -%else %put %str(ERR)OR: load failed for &caslib..&table; +/* ---- get list of CAS servers ----------------------------------------- */ +%let base_uri=%mf_getplatform(VIYARESTAPI); +%let fname1=%mf_getuniquefileref(); +%let libref1=%mf_getuniquelibref(); + +proc http method='GET' out=&fname1 oauth_bearer=sas_services + url="&base_uri/casManagement/servers"; +run; + +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200), + msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) +) + +libname &libref1 JSON fileref=&fname1; + +data _null_; + set &libref1..items; + call symputx(cats('_sv_', _n_), name, 'L'); + call symputx('_svcount', _n_, 'L'); +run; + +libname &libref1 clear; +filename &fname1 clear; + +/* ---- find which server owns this session ------------------------------ */ +%do i=1 %to &_svcount; + %if "&server"="" %then %do; + %if &mdebug=1 %then %put checking server: &&_sv_&i; + %let ftmp=%mf_getuniquefileref(); + proc http method='GET' out=&ftmp oauth_bearer=sas_services + url="&base_uri/casManagement/servers/&&_sv_&i/sessions/&uuid"; + run; + %if &SYS_PROCHTTP_STATUS_CODE=200 + %then %let server=&&_sv_&i; + filename &ftmp clear; + %end; +%end; + +%mp_abort( + iftrue=("&server"=""), + msg=%str(Could not find owning server for CAS session &uuid) +) + +%if &mdebug=1 %then %put &=server; + +/* ---- discover source file from REST endpoint -------------------------- */ +%let fname1=%mf_getuniquefileref(); +%let libref1=%mf_getuniquelibref(); + +proc http method='GET' out=&fname1 oauth_bearer=sas_services + url="&base_uri/casManagement/servers/&server/caslibs/&caslib/tables/&table"; +run; + +%if &mdebug=1 %then %do; + %put &=SYS_PROCHTTP_STATUS_CODE &=SYS_PROCHTTP_STATUS_PHRASE; + data _null_; + infile &fname1; + input; + putlog _infile_; + run; +%end; + +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE=404), + msg=%str(&caslib..&table not found - is a source file registered?) +) +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200), + msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) +) + +libname &libref1 JSON fileref=&fname1; + +data _null_; + set &libref1..tablereference; + call symputx('srcfile', sourceTableName, 'L'); + call symputx('srccaslib', sourceCaslibName, 'L'); + stop; +run; + +libname &libref1 clear; +filename &fname1 clear; + +%mp_abort( + iftrue=("&srcfile"="" or "&srccaslib"=""), + msg=%str(No sourceTableName/sourceCaslibName for &caslib..&table) +) + +%if &mdebug=1 %then %put &=srcfile &=srccaslib; + +/* ---- load from discovered source -------------------------------------- */ +proc casutil; + load casdata="&srcfile" + incaslib="&srccaslib" + casout="&table" + outcaslib="&caslib" + promote; +quit; + +%mp_abort( + iftrue=(&syscc ne 0), + msg=%str(Load failed for &caslib..&table) +) + +%put NOTE: Table &caslib..&table loaded and promoted from &srcfile; /* ---- restore options --------------------------------------------------- */ %if &mdebug=1 %then %do; options &_sysopts; %end; -%mend mv_castabload; \ No newline at end of file +%mend mv_castabload; From f642e35f6b04212856c07236fea9dc364906d37a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Apr 2026 17:48:38 +0000 Subject: [PATCH 11/13] chore: updating all.sas --- all.sas | 225 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 160 insertions(+), 65 deletions(-) diff --git a/all.sas b/all.sas index 3d00f66..6cdd079 100644 --- a/all.sas +++ b/all.sas @@ -24415,74 +24415,79 @@ run; ) %mend mfv_getpathuri;/** @file mv_castabload.sas - @brief Checks if a CAS table exists in a CASLIB; if not, loads & promotes it - @details Runs in SPRE against an active CAS session. Uses - `table.tableExists` to check whether the table is already in-memory, - and PROC CASUTIL LOAD with the PROMOTE option to load it if not. - CASUTIL infers the file type from the source file extension. + @brief Checks if a CAS table is loaded; if not, loads and promotes it + @details Runs in SPRE against an active CAS session. Accepts a + SAS libref, derives the CAS caslib and session UUID from + sashelp.vlibnam, then checks whether the table is already + in-memory. If not, locates the owning CAS server via the + casManagement REST API, queries the table endpoint to discover + the source file and caslib, then loads and promotes the table. A CAS session must already be established by the caller, eg: cas mysess; - %mv_castabload(caslib=Public, table=BASEBALL) + libname mylib cas caslib=Public; + %mv_castabload(lib=mylib, table=BASEBALL) - or (if not a hdat source with the same name as the table): - - %mv_castabload(caslib=Public, table=BASEBALL, - srcfile=MYBASEBALL.parquet) - - @param [in] caslib= CASLIB containing the source file - @param [in] table= Name to give the in-memory CAS table - @param [in] srcfile= (0) Source file name.ext in the caslib. If not provided, - the code assumes that srcfile=&table..sashdat - @param [in] mdebug= (0) Set to 1 to enable verbose logging: + @param [in] lib= SAS libref for the CAS caslib + @param [in] table= Name of the CAS table to load + @param [in] mdebug= (0) Set to 1 to enable verbose logging: - echoes resolved parameters - prints tableExists result - enables mprint/notes during PROC calls - @returns Sets global macro variable `MV_CASTABLOAD_RC`: - 0 = table already existed (no load performed) - 1 = table was loaded & promoted successfully - 3 = action failed (including source file missing) -

SAS Macros

- @li mfv_existsashdat.sas + @li mf_getplatform.sas + @li mf_getuniquefileref.sas + @li mf_getuniquelibref.sas + @li mp_abort.sas **/ %macro mv_castabload( - caslib= + lib= ,table= - ,srcfile=0 ,mdebug=0 ); -%global MV_CASTABLOAD_RC; -%let MV_CASTABLOAD_RC=3; - -%local _sysopts; +%local _sysopts base_uri caslib uuid server + srcfile srccaslib fname1 libref1 ftmp i _svcount _exists; %let _sysopts=%sysfunc(getoption(mprint)) %sysfunc(getoption(notes)); /* ---- input validation -------------------------------------------------- */ -%if "&caslib"="" or "&table"="" or "&srcfile"="" %then %do; - %put %str(ERR)OR: caslib=, table= and srcfile= are all required; - %return; -%end; -%if "&srcfile"="0" %then %let srcfile=&table..sashdat; +%mp_abort( + iftrue=("&lib"="" or "&table"=""), + msg=%str(lib= and table= are required) +) %if &mdebug=1 %then %do; - %put &=caslib; + %put &=lib; %put &=table; - %put &=srcfile; options mprint notes; %end; -/* ---- check source file exists ------------------------------------------ */ -%if not %mfv_existsashdat(&caslib..%scan(&srcfile,1,.)) %then %do; - %put %str(ERR)OR: Source file "&srcfile" not found in caslib "&caslib"; - %let MV_CASTABLOAD_RC=3; - %return; -%end; +/* ---- derive caslib and session UUID from sashelp.vlibnam --------------- */ +data _null_; + set sashelp.vlibnam( + where=(libname="%upcase(&lib)" + and sysname in ("Caslib","Session UUID")) + ); + if sysname="Caslib" then call symputx('caslib',sysvalue,'L'); + else call symputx('uuid',sysvalue,'L'); + %if &mdebug=1 %then %do; + putlog sysname sysvalue; + %end; +run; + +%mp_abort( + iftrue=("&caslib"=""), + msg=%str(&lib is not an assigned CAS libref) +) + +%mp_abort( + iftrue=("&uuid"=""), + msg=%str(No session UUID found for libref &lib) +) /* ---- existence check --------------------------------------------------- */ proc cas; @@ -24492,40 +24497,130 @@ proc cas; %if &mdebug=1 %then %do; print r; %end; - if r.exists = 0 then rc = 9; - else rc = 0; - symputx('MV_CASTABLOAD_RC', rc, 'G'); + if r.exists > 0 then call symputx('_exists', '1', 'L'); + else call symputx('_exists', '0', 'L'); quit; - -/* ---- load if absent ---------------------------------------------------- */ -%if &MV_CASTABLOAD_RC=9 %then %do; - - proc casutil; - load casdata="&srcfile" - incaslib="&caslib" - casout="&table" - outcaslib="&caslib" - promote; - quit; - - %if &syserr=0 %then %let MV_CASTABLOAD_RC=1; - %else %let MV_CASTABLOAD_RC=3; - +/* ---- already loaded: skip ---------------------------------------------- */ +%if &_exists=1 %then %do; + %put NOTE: Table &caslib..&table already loaded - skipping; + %return; %end; -%if &MV_CASTABLOAD_RC=0 %then - %put NOTE: Table &caslib..&table already loaded - skipping; -%else %if &MV_CASTABLOAD_RC=1 %then - %put NOTE: Table &caslib..&table loaded and promoted; -%else %put %str(ERR)OR: load failed for &caslib..&table; +/* ---- get list of CAS servers ----------------------------------------- */ +%let base_uri=%mf_getplatform(VIYARESTAPI); +%let fname1=%mf_getuniquefileref(); +%let libref1=%mf_getuniquelibref(); + +proc http method='GET' out=&fname1 oauth_bearer=sas_services + url="&base_uri/casManagement/servers"; +run; + +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200), + msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) +) + +libname &libref1 JSON fileref=&fname1; + +data _null_; + set &libref1..items; + call symputx(cats('_sv_', _n_), name, 'L'); + call symputx('_svcount', _n_, 'L'); +run; + +libname &libref1 clear; +filename &fname1 clear; + +/* ---- find which server owns this session ------------------------------ */ +%do i=1 %to &_svcount; + %if "&server"="" %then %do; + %if &mdebug=1 %then %put checking server: &&_sv_&i; + %let ftmp=%mf_getuniquefileref(); + proc http method='GET' out=&ftmp oauth_bearer=sas_services + url="&base_uri/casManagement/servers/&&_sv_&i/sessions/&uuid"; + run; + %if &SYS_PROCHTTP_STATUS_CODE=200 + %then %let server=&&_sv_&i; + filename &ftmp clear; + %end; +%end; + +%mp_abort( + iftrue=("&server"=""), + msg=%str(Could not find owning server for CAS session &uuid) +) + +%if &mdebug=1 %then %put &=server; + +/* ---- discover source file from REST endpoint -------------------------- */ +%let fname1=%mf_getuniquefileref(); +%let libref1=%mf_getuniquelibref(); + +proc http method='GET' out=&fname1 oauth_bearer=sas_services + url="&base_uri/casManagement/servers/&server/caslibs/&caslib/tables/&table"; +run; + +%if &mdebug=1 %then %do; + %put &=SYS_PROCHTTP_STATUS_CODE &=SYS_PROCHTTP_STATUS_PHRASE; + data _null_; + infile &fname1; + input; + putlog _infile_; + run; +%end; + +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE=404), + msg=%str(&caslib..&table not found - is a source file registered?) +) +%mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200), + msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) +) + +libname &libref1 JSON fileref=&fname1; + +data _null_; + set &libref1..tablereference; + call symputx('srcfile', sourceTableName, 'L'); + call symputx('srccaslib', sourceCaslibName, 'L'); + stop; +run; + +libname &libref1 clear; +filename &fname1 clear; + +%mp_abort( + iftrue=("&srcfile"="" or "&srccaslib"=""), + msg=%str(No sourceTableName/sourceCaslibName for &caslib..&table) +) + +%if &mdebug=1 %then %put &=srcfile &=srccaslib; + +/* ---- load from discovered source -------------------------------------- */ +proc casutil; + load casdata="&srcfile" + incaslib="&srccaslib" + casout="&table" + outcaslib="&caslib" + promote; +quit; + +%mp_abort( + iftrue=(&syscc ne 0), + msg=%str(Load failed for &caslib..&table) +) + +%put NOTE: Table &caslib..&table loaded and promoted from &srcfile; /* ---- restore options --------------------------------------------------- */ %if &mdebug=1 %then %do; options &_sysopts; %end; -%mend mv_castabload;/** +%mend mv_castabload; +/** @file mv_castabsave.sas @brief Saves an in-memory CAS table back to persistent storage @details Runs in SPRE against an active CAS session. Accepts a From f1ac0bd82107afa1ab9f61a3b818046b3ae09fc5 Mon Sep 17 00:00:00 2001 From: 4gl <@> Date: Tue, 28 Apr 2026 19:06:29 +0100 Subject: [PATCH 12/13] fix: removing usecache option (wasn't used in the end) --- tests/viyaonly/mfv_existsashdat.test.sas | 27 +----------------------- viya/mfv_existsashdat.sas | 18 ++++++---------- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/tests/viyaonly/mfv_existsashdat.test.sas b/tests/viyaonly/mfv_existsashdat.test.sas index 1dd2426..1cf5dde 100644 --- a/tests/viyaonly/mfv_existsashdat.test.sas +++ b/tests/viyaonly/mfv_existsashdat.test.sas @@ -63,37 +63,12 @@ quit; ) /* ------------------------------------------------------------------------ */ -%put TEST 3 - usecache= controls whether the cached dataset is reused; +/* Teardown */ /* ------------------------------------------------------------------------ */ - -/* First call: populates the cache dataset work.testcache_&testcaslib */ -%let _rc=%mfv_existsashdat(&testcaslib..&tab1,outprefix=work.testcache); - -/* Delete the sashdat from the caslib so a fresh scan would return 0 */ proc casutil; deletesource casdata="&tab1..sashdat" incaslib="&testcaslib" quiet; quit; -/* usecache=1 (default): must return 1 from the cached dataset */ -%mp_assert( - iftrue=(%mfv_existsashdat(&testcaslib..&tab1,outprefix=work.testcache)=1), - desc=Check returns 1 from cache even after source file is deleted -) - -/* usecache=0: forces rescan and reflects the deletion */ -%mp_assert( - iftrue=( - %mfv_existsashdat(&testcaslib..&tab1,usecache=0,outprefix=work.testcache)=0 - ), - desc=Check returns 0 when usecache=0 forces a rescan after source file deleted -) - -%let syscc=0; - - -/* ------------------------------------------------------------------------ */ -/* Teardown: terminate CAS session (sashdat already removed in TEST 4) */ -/* ------------------------------------------------------------------------ */ cas mysess terminate; %let syscc=0; diff --git a/viya/mfv_existsashdat.sas b/viya/mfv_existsashdat.sas index 84d47cd..65be9e3 100644 --- a/viya/mfv_existsashdat.sas +++ b/viya/mfv_existsashdat.sas @@ -8,15 +8,12 @@ The function uses `dosubl()` to run the `table.fileinfo` action, for the specified library, filtering for `*.sashdat` tables. - IMPORTANT NOTE - The results are cached in a WORK table (&outprefix._&lib). - If that table already exists, it is queried instead, to avoid the - dosubl() performance hit. - - To force a rescan, just use a new `&outprefix` value, or delete the table(s) - before running the function. + Results are cached in a WORK table (&outprefix._&lib). If that table + already exists it is queried directly to avoid the dosubl() overhead. + To force a rescan, use a new `&outprefix` value or delete the cache + table before calling. @param [in] libds library.dataset - @param [in] usecache= (1) Set to 0 to rebuild the cache @param [out] outprefix= (work.mfv_existsashdat) Used to store current HDATA tables to improve subsequent query performance. This reference is a prefix and is converted to `&prefix._{libref}` @@ -27,14 +24,13 @@ @author Mathieu Blauw **/ -%macro mfv_existsashdat(libds,usecache=1,outprefix=work.mfv_existsashdat -); +%macro mfv_existsashdat(libds,outprefix=work.mfv_existsashdat); %local rc dsid name lib ds; %let lib=%upcase(%scan(&libds,1,'.')); %let ds=%upcase(%scan(&libds,-1,'.')); -/* if table does not exist, create it */ -%if &usecache ne 1 or %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; +/* if cache table does not exist, build it */ +%if %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; %let rc=%sysfunc(dosubl(%nrstr( /* Read in table list (once per &lib per session) */ proc cas; From 955854919f1370c44060c5bb644a332c671cdbdb Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Apr 2026 18:07:10 +0000 Subject: [PATCH 13/13] chore: updating all.sas --- all.sas | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/all.sas b/all.sas index 6cdd079..27c485e 100644 --- a/all.sas +++ b/all.sas @@ -24216,15 +24216,12 @@ run; The function uses `dosubl()` to run the `table.fileinfo` action, for the specified library, filtering for `*.sashdat` tables. - IMPORTANT NOTE - The results are cached in a WORK table (&outprefix._&lib). - If that table already exists, it is queried instead, to avoid the - dosubl() performance hit. - - To force a rescan, just use a new `&outprefix` value, or delete the table(s) - before running the function. + Results are cached in a WORK table (&outprefix._&lib). If that table + already exists it is queried directly to avoid the dosubl() overhead. + To force a rescan, use a new `&outprefix` value or delete the cache + table before calling. @param [in] libds library.dataset - @param [in] usecache= (1) Set to 0 to rebuild the cache @param [out] outprefix= (work.mfv_existsashdat) Used to store current HDATA tables to improve subsequent query performance. This reference is a prefix and is converted to `&prefix._{libref}` @@ -24235,14 +24232,13 @@ run; @author Mathieu Blauw **/ -%macro mfv_existsashdat(libds,usecache=1,outprefix=work.mfv_existsashdat -); +%macro mfv_existsashdat(libds,outprefix=work.mfv_existsashdat); %local rc dsid name lib ds; %let lib=%upcase(%scan(&libds,1,'.')); %let ds=%upcase(%scan(&libds,-1,'.')); -/* if table does not exist, create it */ -%if &usecache ne 1 or %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; +/* if cache table does not exist, build it */ +%if %sysfunc(exist(&outprefix._&lib)) ne 1 %then %do; %let rc=%sysfunc(dosubl(%nrstr( /* Read in table list (once per &lib per session) */ proc cas;