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;