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/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/all.sas b/all.sas
index 447822e..27c485e 100644
--- a/all.sas
+++ b/all.sas
@@ -3512,6 +3512,7 @@ run;
run;
proc sql;
drop table &ds;
+ quit;
%mend mp_assert;/**
@file
@@ -4042,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;
@@ -4079,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');
@@ -4092,6 +4093,7 @@ run;
run;
proc sql;
drop table &ds;
+ quit;
%end;
%mend mp_assertscope;
@@ -10103,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_;
@@ -24211,12 +24214,12 @@ 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.
- 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 [out] outprefix= (work.mfv_existsashdat)
@@ -24229,13 +24232,12 @@ run;
@author Mathieu Blauw
**/
-%macro mfv_existsashdat(libds,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 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) */
@@ -24246,7 +24248,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;
)));
@@ -24263,6 +24265,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
@@ -24371,6 +24410,405 @@ run;
msg=Cannot leave &sysmacroname with syscc=&syscc
)
%mend mfv_getpathuri;/**
+ @file mv_castabload.sas
+ @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;
+ libname mylib cas caslib=Public;
+ %mv_castabload(lib=mylib, table=BASEBALL)
+
+ @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
+
+
SAS Macros
+ @li mf_getplatform.sas
+ @li mf_getuniquefileref.sas
+ @li mf_getuniquelibref.sas
+ @li mp_abort.sas
+
+**/
+
+%macro mv_castabload(
+ lib=
+ ,table=
+ ,mdebug=0
+);
+
+%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 -------------------------------------------------- */
+%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');
+ %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;
+ table.tableExists result=r /
+ caslib="&caslib"
+ name="&table";
+ %if &mdebug=1 %then %do;
+ print r;
+ %end;
+ if r.exists > 0 then call symputx('_exists', '1', 'L');
+ else call symputx('_exists', '0', 'L');
+quit;
+
+/* ---- already loaded: skip ---------------------------------------------- */
+%if &_exists=1 %then %do;
+ %put NOTE: Table &caslib..&table already loaded - skipping;
+ %return;
+%end;
+
+/* ---- 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;
+/**
+ @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.
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/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;
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/mfv_existsashdat.test.sas b/tests/viyaonly/mfv_existsashdat.test.sas
new file mode 100644
index 0000000..1cf5dde
--- /dev/null
+++ b/tests/viyaonly/mfv_existsashdat.test.sas
@@ -0,0 +1,74 @@
+/**
+ @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
+)
+
+/* ------------------------------------------------------------------------ */
+/* Teardown */
+/* ------------------------------------------------------------------------ */
+proc casutil;
+ deletesource casdata="&tab1..sashdat" incaslib="&testcaslib" quiet;
+quit;
+
+cas mysess terminate;
+
+%let syscc=0;
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/tests/viyaonly/mv_castabload.test.sas b/tests/viyaonly/mv_castabload.test.sas
new file mode 100644
index 0000000..6c6d3f9
--- /dev/null
+++ b/tests/viyaonly/mv_castabload.test.sas
@@ -0,0 +1,152 @@
+/**
+ @file
+ @brief Testing mv_castabload macro
+
+ SAS Macros
+ @li mf_uid.sas
+ @li mp_assert.sas
+ @li mp_assertscope.sas
+ @li mv_castabload.sas
+
+**/
+
+options mprint;
+
+/* -------------------------------------------------------------------- */
+/* Setup: start a CAS session and stage a source file in the caslib */
+/* -------------------------------------------------------------------- */
+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();
+
+/* 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;
+ save casdata="&tab1" incaslib="&testcaslib"
+ casout="&tab1..sashdat" outcaslib="&testcaslib" replace;
+ droptable casdata="&tab1" incaslib="&testcaslib" quiet;
+quit;
+
+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
+)
+
+%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=(&_tabexists=1),
+ desc=Check table is in memory after mv_castabload
+)
+
+
+/* -------------------------------------------------------------------- */
+%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=(&_modified=1),
+ desc=Check sentinel row is present in memory before reload
+)
+
+/* Drop the table and reload - source file does not have the sentinel */
+proc casutil;
+ droptable casdata="&tab1" 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;
+
+%let syscc=0;
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/mfv_existsashdat.sas b/viya/mfv_existsashdat.sas
index 5a256c9..65be9e3 100644
--- a/viya/mfv_existsashdat.sas
+++ b/viya/mfv_existsashdat.sas
@@ -6,12 +6,12 @@
%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.
- 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 [out] outprefix= (work.mfv_existsashdat)
@@ -24,13 +24,12 @@
@author Mathieu Blauw
**/
-%macro mfv_existsashdat(libds,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 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) */
@@ -41,7 +40,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/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;
diff --git a/viya/mv_castabload.sas b/viya/mv_castabload.sas
new file mode 100644
index 0000000..4bdb13a
--- /dev/null
+++ b/viya/mv_castabload.sas
@@ -0,0 +1,207 @@
+/**
+ @file mv_castabload.sas
+ @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;
+ libname mylib cas caslib=Public;
+ %mv_castabload(lib=mylib, table=BASEBALL)
+
+ @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
+
+ SAS Macros
+ @li mf_getplatform.sas
+ @li mf_getuniquefileref.sas
+ @li mf_getuniquelibref.sas
+ @li mp_abort.sas
+
+**/
+
+%macro mv_castabload(
+ lib=
+ ,table=
+ ,mdebug=0
+);
+
+%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 -------------------------------------------------- */
+%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');
+ %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;
+ table.tableExists result=r /
+ caslib="&caslib"
+ name="&table";
+ %if &mdebug=1 %then %do;
+ print r;
+ %end;
+ if r.exists > 0 then call symputx('_exists', '1', 'L');
+ else call symputx('_exists', '0', 'L');
+quit;
+
+/* ---- already loaded: skip ---------------------------------------------- */
+%if &_exists=1 %then %do;
+ %put NOTE: Table &caslib..&table already loaded - skipping;
+ %return;
+%end;
+
+/* ---- 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;
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;