@sasjs/core 6.1.3 moved the derived variable field rules out of mp_derivedvars into mp_derivedvarcheck, so the same scan can run before a definition is used. DC carried its own copy of that list in mpe_derived_check.sas, which is now deleted: the post edit hook calls the core macro instead, and the hook keeps only the rules that belong to the control table rather than the expression - LIBREF/DSN required, and ACTIVE within the picker pair. The scan is stricter than the copy it replaces, in three ways: - VAR_NM may not be _N_, _ERROR_ or _IORC_ (automatic variables of the data step) - VAR_LOGIC may not exceed 4000 characters - VAR_LOGIC may not call a function that submits code or reaches the file system, the environment or the macro symbol table - a function name cannot be assembled at run time, so this is a control the expression cannot evade and it relaxes one: VAR_LABEL may carry a line break, which is flattened to a space when the label is used, so only the remaining control characters are refused. The mock hook mirror is updated to the same rules, and stagedata.test.4 gains the two submit-time cases that pin the new wiring.
146 lines
5.7 KiB
SAS
146 lines
5.7 KiB
SAS
/**
|
|
@file
|
|
@brief Generates the SAS statements that create Data Controller derived variables
|
|
@details Derived (computed) variables are configured in MPE_DERIVED. Each
|
|
row defines one variable, which is created in a DATA step view ahead of the
|
|
filter, so that it can be filtered on in the EDIT screen.
|
|
|
|
The engine lives in @sasjs/core as mp_derivedvars - this macro only supplies
|
|
what is specific to Data Controller: the control table name, the bitemporal
|
|
filter that picks the current version of a row, and the list of names that
|
|
belong to the metadata and may not be used as a variable.
|
|
|
|
The macro writes the generated statements to a fileref, so that callers can
|
|
compose them into a view:
|
|
|
|
%mpe_derived(libds=&libds,dclib=&mpelib,outref=dercode)
|
|
data work.vw_derived / view=work.vw_derived;
|
|
set &libds;
|
|
%include dercode;
|
|
run;
|
|
|
|
Alternatively, pass outview= and the macro will build the view itself.
|
|
|
|
The filter must be applied by the caller, in the step that reads the view:
|
|
|
|
data out;
|
|
set work.vw_derived;
|
|
where %inc filtref;;
|
|
run;
|
|
|
|
A WHERE statement applies to the data set named on the SET statement, so it
|
|
cannot reference a variable that is created in the same data step. Applying
|
|
it to the view puts the derived variables on the input, which is what makes
|
|
them filterable.
|
|
|
|
Each statement block is of the form:
|
|
|
|
attrib REGION length=$21 label='REGION=<expression>' format=$21.;
|
|
REGION = <expression>;
|
|
|
|
Type and length are declared by the administrator, so the result is
|
|
deterministic. VAR_LABEL is optional: when it is missing or blank the label
|
|
is the variable name, an equals sign and the expression, otherwise the
|
|
configured label is used as given.
|
|
|
|
VAR_DEPS lists every input the expression uses - physical columns and
|
|
derived variables alike. The statements are generated in dependency order,
|
|
so a derived variable may build on another one without a sequence number
|
|
being maintained. A cycle is rejected.
|
|
|
|
Semicolons, macro triggers and comments are rejected, so a row can only ever
|
|
be an expression, and an expression that would let the row submit code of its
|
|
own - DOSUBL, or a function reaching the file system, the environment or the
|
|
macro symbol table - is refused by name.
|
|
|
|
The field level rules themselves are core's: mp_derivedvarcheck scans the
|
|
definitions and mp_derivedvars calls it, which is the same macro the MPE_DERIVED
|
|
post edit hook runs at submit time (services/hooks/mpe_derived_postedit). A
|
|
row is therefore measured against one rule list, whether it is being staged
|
|
or read.
|
|
|
|
A derived variable may not reuse the name of a physical column, and may not
|
|
reference itself: an intra-row calculation would imply a retained value,
|
|
which depends on the order rows happen to arrive in.
|
|
|
|
<h4> SAS Macros </h4>
|
|
@li mp_derivedvars.sas
|
|
@li mp_abort.sas
|
|
|
|
@param [in] libds= The target table (libref.dataset, lib must be assigned)
|
|
@param [in] dclib= The Data Controller control library
|
|
@param [out] outref= (0) Fileref to receive the generated statements
|
|
@param [out] outvars= (work.derived_vars) Dataset of derived variable names
|
|
@param [in] outview= (0) Optional two-level name for the derived view
|
|
|
|
@version 9.2
|
|
@author 4GL Apps Ltd
|
|
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
|
|
and may not be re-distributed or re-sold without the express permission of
|
|
4GL Apps Ltd.
|
|
|
|
**/
|
|
|
|
%macro mpe_derived(libds=,
|
|
dclib=,
|
|
outref=0,
|
|
outvars=work.derived_vars,
|
|
outview=0
|
|
);
|
|
|
|
%local reserved;
|
|
|
|
/* the only name that has to be refused outright - the metadata columns of the
|
|
target table are physical columns, so the engine's collision check covers
|
|
them whatever MPE_TABLES calls them */
|
|
%let reserved=%str(_____DELETE__THIS__RECORD_____);
|
|
|
|
/* dc_dttmtfmt is the bitemporal cut-off used in the where= below - callers get
|
|
it from %mpeinit(). Unset, the filter degenerates to "lt tx_to" and the
|
|
control-table step fails with a syntax error a long way from the cause. */
|
|
%mp_abort(iftrue= (%length(&libds)=0 or %length(&dclib)=0 or %length(&dc_dttmtfmt)=0)
|
|
,mac=&sysmacroname
|
|
,msg=%str(Missing: libds dclib dc_dttmtfmt)
|
|
)
|
|
|
|
/* A format catalog is addressed as LIBREF.CATALOGNAME-FC (the form
|
|
mpe_validatecol accepts, and the one viewtables returns). A catalog has no
|
|
observations, so it can have no derived variables - and it is not a dataset,
|
|
so the engine's existence check would abort the caller. Return an empty
|
|
variable list and an empty statement file instead, and leave outview alone:
|
|
a caller only reads the view when the list is not empty. */
|
|
%if %length(&libds)>3 and "%upcase(%substr(&libds,%length(&libds)-2,3))"="-FC" %then %do;
|
|
/* the file has to exist even though it stays empty - callers %include it
|
|
unconditionally, and a fileref with no file behind it fails there */
|
|
%if "%upcase(&outref)" ne "0" %then %do;
|
|
filename &outref temp lrecl=32767;
|
|
data _null_;
|
|
file &outref;
|
|
run;
|
|
%end;
|
|
/* The shape is the engine's own: mp_derivedvars builds
|
|
`data &outvars(keep=name); length name $32; name=''; if 0; run;` before it
|
|
returns (empty case, and again from the sorted list in the populated case),
|
|
so NAME is the only column either path can produce. Every caller reads
|
|
NAME alone - getdata's `where a.NAME in (select NAME from
|
|
work.derived_vars)` is the only projection of this dataset in the SAS tree. */
|
|
data &outvars(keep=name);
|
|
length name $32;
|
|
name='';
|
|
if 0;
|
|
run;
|
|
%return;
|
|
%end;
|
|
|
|
%mp_derivedvars(
|
|
libds=&libds
|
|
,control=&dclib..mpe_derived
|
|
,where=%str(&dc_dttmtfmt lt tx_to and active=1)
|
|
,outref=&outref
|
|
,outvars=&outvars
|
|
,outview=&outview
|
|
,reserved=&reserved
|
|
)
|
|
|
|
%mend mpe_derived;
|