feat(editor): serve a truncated selection with a warning instead of aborting
Build / Build-and-ng-test (pull_request) Failing after 3m13s
Build / Build-and-test-development (pull_request) Skipped
Lighthouse Checks / lighthouse (pull_request) Successful in 19m53s

A selection over DC_MAXOBS_WEBEDIT or DC_MAXCELLS_WEBEDIT was refused with
"Table is too big", leaving the user with no grid and no way to see what the
filter had matched.  getdata now serves the first rows that fit and flags it:

- the row cap is min(DC_MAXOBS_WEBEDIT, DC_MAXCELLS_WEBEDIT / columns), so a
  wide table is still bounded by the cell budget
- sasparams carries TRUNCATED (0/1) and TRUNC_MSG, naming the limit reached
- the EDIT screen shows a warning triangle next to the table name, with the
  message in a tooltip and the advice to filter

The mock mirrors the new behaviour, the Cypress spec asserts the truncated
grid and the tooltip (real hover, opacity checked so a hidden tooltip cannot
pass), and the SAS limit test now asserts the served row counts and message
rather than a cancelled child job.
This commit is contained in:
dc committed 2026-10-22 16:38:37 +00:00
1 parent 779006b0bf
commit 34691dceec
8 files changed
+286 -168

No files matched your search

+82 -51
View File
@@ -1,13 +1,15 @@
// The EDIT screen refuses a selection that is too large - DC_MAXOBS_WEBEDIT
// rows, or DC_MAXCELLS_WEBEDIT cells (rows x columns), whichever is reached
// first - and the VIEW screen caps at DC_MAXOBS_WEBVIEW rows.
// The EDIT screen is bounded by DC_MAXOBS_WEBEDIT rows and DC_MAXCELLS_WEBEDIT
// cells (rows x columns), whichever is reached first, and the VIEW screen caps
// at DC_MAXOBS_WEBVIEW rows.
//
// A selection over an EDIT limit is served TRUNCATED - the grid carries the
// first rows that fit, and a warning triangle against the table name says why.
// This spec pins those limits against the mock, which mirrors the shipped
// defaults (250 rows / 200000 cells for EDIT, 2000 rows for VIEW). It uses
// two existing fixtures rather than new ones:
// MPE_X_SEARCH - 1000 rows x 9 cols, so it trips the ROW limit
// MPE_X_WIDE - 200 rows x 1001 cols (200,400 cells), under the row limit
// but over the CELL limit
// MPE_X_WIDE - 200 rows x 1001 cols (200,200 cells), under the row limit
// but over the CELL limit, so the row cap is 199
export {}
const hostUrl = Cypress.env('hosturl')
@@ -60,27 +62,51 @@ context('EDIT row and cell limits, VIEW row limit: ', function () {
})
})
it('2 | EDIT refuses a table over the row limit', () => {
it('2 | EDIT serves a table over the row limit, truncated, with a warning', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_search')
openEdit()
cy.get('.abortMsg', { timeout: longerCommandTimeout }).should(
// 1000 rows x 9 columns is over the 250 row limit, so the grid carries the
// first 250 rows and the header says how many it is showing.
cy.get('.editor-title', { timeout: longerCommandTimeout }).should(
'contain.text',
'Table is too big (1000 rows) - please filter and try again!'
'250 rows'
)
cy.get('#hotTable .ht_master .htCore tbody tr', {
timeout: longerCommandTimeout
}).should('have.length.at.least', 1)
cy.get('.truncation-warning', { timeout: longerCommandTimeout })
.should('be.visible')
.realHover()
cy.get('.editor-title clr-tooltip-content', {
timeout: longerCommandTimeout
})
.should('be.visible')
.should('contain.text', 'DC_MAXOBS_WEBEDIT')
.should(($el) => expect($el.css('opacity')).to.eq('1'))
})
it('3 | EDIT refuses a table over the cell limit', () => {
it('3 | EDIT serves a table over the cell limit, truncated, with a warning', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_wide')
openEdit()
// 200 rows x 1002 columns (the 1001 stored columns plus the housekeeping
// delete column) = 200,400 cells, over the 200,000 cell limit, while the
// row count is under the 250 row limit.
cy.get('.abortMsg', { timeout: longerCommandTimeout }).should(
// 200 rows x 1001 stored columns = 200,200 cells, over the 200,000 cell
// limit, while the row count is under the 250 row limit - so the cell
// limit sets the cap: floor(200000 / 1001) = 199 rows.
cy.get('.editor-title', { timeout: longerCommandTimeout }).should(
'contain.text',
'Selection is too wide (200 rows x 1002 cols) - please filter and try again!'
'199 rows'
)
cy.get('.truncation-warning', { timeout: longerCommandTimeout })
.should('be.visible')
.realHover()
cy.get('.editor-title clr-tooltip-content', {
timeout: longerCommandTimeout
})
.should('be.visible')
.should('contain.text', 'DC_MAXCELLS_WEBEDIT')
.should(($el) => expect($el.css('opacity')).to.eq('1'))
})
it('4 | the EDIT limits follow MPE_CONFIG, not the shipped defaults', () => {
@@ -90,20 +116,29 @@ context('EDIT row and cell limits, VIEW row limit: ', function () {
// all. And MPE_CONFIG lives in the control library, so reading it from the
// table's own library would fail for this table in particular.
//
// The cell limit is the one to move: 7 rows x 18 columns is 126 cells, so
// at 110 the TESTDATA.MPE_X_NEW edit must be refused, where a hard-coded
// 200000 - or a config read that fails and falls back to it - lets it
// through. 110 is still above MPE_CONFIG's own 17 x 6 = 102, so the option
// can be put back afterwards.
// The cell limit is the one to move: 7 rows x 17 columns is 119 cells, so
// at 110 the TESTDATA.MPE_X_NEW edit must be truncated to
// floor(110 / 17) = 6 rows, where a hard-coded 200000 - or a config read
// that fails and falls back to it - lets it through. 110 is still above
// MPE_CONFIG's own 6 x 17 = 102, so the option can be put back afterwards.
setConfigValue('DC_MAXCELLS_WEBEDIT', '110', () => {
visitPage('view/data/TESTDATA.MPE_X_NEW')
closeAbortModalIfPresent()
openEdit()
cy.get('.abortMsg', { timeout: longerCommandTimeout }).should(
cy.get('.editor-title', { timeout: longerCommandTimeout }).should(
'contain.text',
'Selection is too wide (7 rows x 18 cols) - please filter and try again!'
'6 rows'
)
cy.get('.truncation-warning', { timeout: longerCommandTimeout })
.should('be.visible')
.realHover()
cy.get('.editor-title clr-tooltip-content', {
timeout: longerCommandTimeout
})
.should('be.visible')
.should('contain.text', 'DC_MAXCELLS_WEBEDIT')
.should(($el) => expect($el.css('opacity')).to.eq('1'))
})
// Put the option back, so the rest of the estate sees the shipped default.
@@ -112,28 +147,31 @@ context('EDIT row and cell limits, VIEW row limit: ', function () {
closeAbortModalIfPresent()
openEdit()
// 126 cells under a 200000 cell limit: the editor loads, no abort.
cy.get('#hotTable .ht_master .htCore tbody tr', {
timeout: longerCommandTimeout
}).should('exist')
// 119 cells under a 200000 cell limit: all 7 rows load, no warning.
cy.get('.editor-title', { timeout: longerCommandTimeout }).should(
'contain.text',
'7 rows'
)
cy.get('.truncation-warning').should('not.exist')
})
})
it('5 | the filter picker still lists the variables after the abort', () => {
it('5 | the warning says to filter, and filtering narrows the grid', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_search')
openEdit()
cy.get('.abortMsg', { timeout: longerCommandTimeout }).should(
'contain.text',
'Table is too big (1000 rows) - please filter and try again!'
)
cy.get('.truncation-warning', { timeout: longerCommandTimeout })
.should('be.visible')
.realHover()
cy.get('.editor-title clr-tooltip-content', {
timeout: longerCommandTimeout
})
.should('be.visible')
.should('contain.text', 'Filter the table to work with the rest')
.should(($el) => expect($el.css('opacity')).to.eq('1'))
closeAbortModal()
// The abort returns no table, and the picker's variable list normally comes
// from the table's own columns - so without the public/getcols fallback the
// "please filter and try again" advice has nothing to pick from. Every
// column of MPE_X_SEARCH must be on offer.
// The advice has to be actionable: the picker must offer the table's
// columns and the filter must reload the grid.
openFilterPopup()
filterVariables().should(($options) => {
@@ -155,20 +193,20 @@ context('EDIT row and cell limits, VIEW row limit: ', function () {
])
})
// and the suggestion list is usable, not just populated
setFilterVariable('SITE_NAME')
// Submitting is what exercises TYPE. The picker keys its operator set and
// its value quoting on it, and the fallback list never passes through
// mergeColsRules, so without TYPE on this payload a character value would be
// submitted unquoted and the backend's %mp_filtercheck would reject it.
setFilterValue('Bristol')
// 100 of the 1000 rows: under the limit, so nothing is truncated.
setFilterValue('Nereid Trench')
submitFilter()
cy.get('.infoBar-text', { timeout: longerCommandTimeout }).should(
'contain.text',
"SITE_NAME = 'Bristol'"
"SITE_NAME = 'Nereid Trench'"
)
cy.get('.editor-title', { timeout: longerCommandTimeout }).should(
'contain.text',
'100 rows'
)
cy.get('.truncation-warning').should('not.exist')
})
})
@@ -185,13 +223,6 @@ const openEdit = () => {
}).click()
}
/** The abort modal offers Close, and the filter button sits behind it. */
const closeAbortModal = () => {
cy.get('app-info-modal', { timeout: longerCommandTimeout })
.contains('button', 'Close')
.click()
}
/**
* The EDIT screen's Filter button opens the same .filter-modal the viewer uses.
* It only renders once the editor is in its read-only (non-editing) state,
@@ -245,6 +245,25 @@
}
</clr-tooltip>
@if (truncationNote) {
<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="warning"
aria-label="The selection was truncated"
class="color-orange truncation-warning"
size="20"
></clr-icon>
<clr-tooltip-content
clrPosition="right"
clrSize="lg"
*clrIfOpen
>
{{ truncationNote }}
</clr-tooltip-content>
</clr-tooltip>
}
@if (dataSource) {
@if (!zeroFilterRows) {
({{ dataSource.length | thousandSeparator: ',' }}
@@ -160,6 +160,9 @@ export class EditorComponent
dsmeta: DSMeta[] = []
versions: Version[] = []
dsNote = ''
/** Set when getdata served a truncated selection - shown as a warning
* against the table name, with the reason in a tooltip. */
truncationNote = ''
viewboxes = false
@@ -3930,6 +3933,11 @@ export class EditorComponent
this.dsNote = ''
}
// getdata serves a selection over DC_MAXOBS_WEBEDIT / DC_MAXCELLS_WEBEDIT
// truncated to the first rows within the limit, and carries the reason
// here. Empty means the whole selection is in the grid.
this.truncationNote = response.data.sasparams[0].TRUNC_MSG || ''
const hot = this.hotInstance
const approvers: Approver[] = response.data.approvers
+4
View File
@@ -47,6 +47,10 @@ export interface SASParam {
PKCNT: number
RK_FLAG: number
TMVARS: string
/** 1 when getdata cut the selection down to the row / cell limit */
TRUNCATED?: number
/** Reason shown in the tooltip when TRUNCATED is 1, else empty */
TRUNC_MSG?: string
}
export interface ExcelRule {
+52 -44
View File
@@ -191,9 +191,8 @@ if (tableData && tableData.rows && tableData.columns) {
// ─── Build response ──────────────────────────────────────────────────────────
let response
// Set when the row or cell limit is exceeded - mirrors the %mp_abort in
// getdata.sas, and is emitted as sasjsAbort below.
let abortMsg = ''
// Set when the row or cell limit cut the selection down - mirrors the
// TRUNCATED / TRUNC_MSG in sasparams in getdata.sas.
if (tableData && tableData.columns && tableData.rows) {
// Exclude temporal columns (TX_FROM/TX_TO etc.) from the editor view
@@ -236,15 +235,15 @@ if (tableData && tableData.columns && tableData.rows) {
}
// ─── Row and cell limits ─────────────────────────────────────────────────
// Mirrors getdata.sas: DC_MAXOBS_WEBEDIT rows, or DC_MAXCELLS_WEBEDIT
// cells (rows x columns), whichever is reached first. Both are read from
// MPE_CONFIG, the way the real service picks them up from the settings
// program it generates - so editing the option in DC changes what the mock
// allows, and an option that is absent (or switched off, var_active=0)
// falls back to the shipped default. The check runs after the
// PRE_EDIT_HOOK, as the single check in getdata.sas does - a hook may
// replace the data with a larger table. The cell count includes the
// housekeeping delete column, which is what getdata.sas counts.
// Mirrors getdata.sas: the selection is served TRUNCATED, not refused, at
// DC_MAXOBS_WEBEDIT rows or DC_MAXCELLS_WEBEDIT cells (rows x columns),
// whichever is reached first - the row cap is min(rows, cells/columns).
// Both options are read from MPE_CONFIG, the way the real service picks
// them up from the settings program it generates - so editing the option
// in DC changes what the mock serves, and an option that is absent (or
// switched off, var_active=0) falls back to the shipped default. The
// check runs after the PRE_EDIT_HOOK, as the single check in getdata.sas
// does - a hook may replace the data with a larger table.
// MPE_CONFIG lives in the control library, not in the table's own library,
// so this reads mpeDataDir - the same directory the MPE_TABLES lookup above
// uses. Passing the requested table's dataDir made the read fail for every
@@ -252,20 +251,32 @@ if (tableData && tableData.columns && tableData.rows) {
const dcCfg = loadDcConfig(driveRoot, appLoc, mpeDataDir)
const maxObsWebEdit = dcConfigNumber(dcCfg, 'DC_MAXOBS_WEBEDIT', 250)
const maxCellsWebEdit = dcConfigNumber(dcCfg, 'DC_MAXCELLS_WEBEDIT', 200000)
const editColCount = visibleColumns.length + 1
const cellCount = visibleRows.length * editColCount
if (visibleRows.length > maxObsWebEdit) {
abortMsg =
'Table is too big (' +
visibleRows.length +
' rows) - please filter and try again!'
} else if (cellCount > maxCellsWebEdit) {
abortMsg =
'Selection is too wide (' +
visibleRows.length +
' rows x ' +
editColCount +
' cols) - please filter and try again!'
// getdata.sas counts the columns of work.out, i.e. the table's own
// columns - the delete column is client-owned and not part of it.
const editColCount = visibleColumns.length
let maxRows = maxObsWebEdit
let limitVar = 'DC_MAXOBS_WEBEDIT'
if (editColCount > 0) {
const cellRows = Math.floor(maxCellsWebEdit / editColCount)
if (cellRows < maxRows) {
maxRows = cellRows
limitVar = 'DC_MAXCELLS_WEBEDIT'
}
}
if (maxRows < 1) maxRows = 1
let truncated = 0
let truncMsg = ''
if (visibleRows.length > maxRows) {
truncated = 1
// Same wording as the macro getdata.sas sets, so the spec can assert
// on the text the real service would send.
truncMsg =
'Only the first ' +
maxRows +
' rows are loaded - the ' +
limitVar +
' limit has been reached. Filter the table to work with the rest.'
visibleRows = visibleRows.slice(0, maxRows)
}
// Augment rows: add delete flag, normalise keys, convert temporal values to ISO
@@ -371,7 +382,9 @@ if (tableData && tableData.columns && tableData.rows) {
LOADTYPE: tableReg ? tableReg.loadtype || 'UPDATE' : 'UPDATE',
RK_FLAG: tableReg && tableReg.rk_underlying ? 1 : 0,
CLS_FLAG: 0,
ISMAP: 0
ISMAP: 0,
TRUNCATED: truncated,
TRUNC_MSG: truncMsg
}],
versions: [],
xl_rules: []
@@ -386,26 +399,21 @@ if (tableData && tableData.columns && tableData.rows) {
],
maxvarlengths: [], query: [], sasdata: [],
$sasdata: { vars: {} },
sasparams: [{ COLHEADERS: '', FILTER_TEXT: ' ', PKCNT: 0, PK: '', DTVARS: '', DTTMVARS: '', TMVARS: '', LOADTYPE: 'UPDATE', RK_FLAG: 0, CLS_FLAG: 0, ISMAP: 0 }],
sasparams: [{ COLHEADERS: '', FILTER_TEXT: ' ', PKCNT: 0, PK: '', DTVARS: '', DTTMVARS: '', TMVARS: '', LOADTYPE: 'UPDATE', RK_FLAG: 0, CLS_FLAG: 0, ISMAP: 0, TRUNCATED: 0, TRUNC_MSG: '' }],
versions: [], xl_rules: []
}
}
webOutOpen()
if (abortMsg) {
webOutObj([{ MSG: abortMsg, MAC: 'getdata' }], 'sasjsAbort')
webOutObj([{ STATUS: 'ERROR' }], 'sasparams')
} else {
webOutObj(response.approvers, 'approvers')
webOutObj(response.cols, 'cols')
webOutObj(response.dqdata, 'dqdata')
webOutObj(response.dqrules, 'dqrules')
webOutObj(response.dsmeta, 'dsmeta')
webOutObj(response.maxvarlengths, 'maxvarlengths')
webOutObj(response.query, 'query')
webOutObj(response.sasdata, 'sasdata', response.$sasdata)
webOutObj(response.sasparams, 'sasparams')
webOutObj(response.versions, 'versions')
webOutObj(response.xl_rules, 'xl_rules')
}
webOutObj(response.approvers, 'approvers')
webOutObj(response.cols, 'cols')
webOutObj(response.dqdata, 'dqdata')
webOutObj(response.dqrules, 'dqrules')
webOutObj(response.dsmeta, 'dsmeta')
webOutObj(response.maxvarlengths, 'maxvarlengths')
webOutObj(response.query, 'query')
webOutObj(response.sasdata, 'sasdata', response.$sasdata)
webOutObj(response.sasparams, 'sasparams')
webOutObj(response.versions, 'versions')
webOutObj(response.xl_rules, 'xl_rules')
webOutClose()
+37 -21
View File
@@ -17,6 +17,10 @@
@li CLS_FLG - set to 0 if there are no CLS rules (everything editable)
else set to 1 (CLS rules exist)
@li ISMAP - set to 1 if the target DS is an excel map target, else 0
@li TRUNCATED - set to 1 if the selection was cut down to the
DC_MAXOBS_WEBEDIT / DC_MAXCELLS_WEBEDIT limit, else 0
@li TRUNC_MSG - the reason shown against the table name when TRUNCATED=1,
else empty
<h5> approvers </h5>
<h5> dqrules </h5>
@@ -262,30 +266,38 @@ select upcase(loadtype)
)
/* Sort first - the PRE_EDIT_HOOK contract is that it runs on sorted rows -
then run the hook, then check the row and cell limits. Checking once,
after both, means the counts cover everything that could reach the
browser: the rows the selection matched, and anything the hook has added
to work.out. A wide table exhausts the cell budget long before it
reaches the row limit - 250 rows of a 1000 column table is a quarter of
a million cells - so capping rows alone does not bound the work the
browser is asked to do. The payload itself is capped further down, when
work.outdata is built. */
then run the hook, then apply the row and cell limits (below). */
%let sortpk=%sysfunc(coalescec(&sortpk &var_busfrom,_ALL_));
proc sort data=work.out; by &sortPK; run;
%mpe_runhook(PRE_EDIT_HOOK)
/* A selection over a limit is served truncated, not refused: the browser
gets the first rows within the limit and sasparams carries a warning the
EDIT screen shows against the table name. Truncating bounds the browser's
work the same way an abort did, without leaving the user with no grid and
no explanation of what happened. A wide table exhausts the cell budget
long before it reaches the row limit - 250 rows of a 1000 column table is
a quarter of a million cells - so the row cap is min(rows, cells/columns).
Counting after the PRE_EDIT_HOOK means the cap covers anything the hook
added to work.out. */
%let obscnt=%mf_getattrn(work.out,NLOBS);
%let colcnt=%sysfunc(countw(%mf_getvarlist(work.out)));
%let cellcnt=%eval(&obscnt * &colcnt);
%mp_abort(iftrue=(&obscnt>&DC_MAXOBS_WEBEDIT)
,mac=&_program
,msg=Table is too big (&obscnt rows) - please filter and try again!
)
%mp_abort(iftrue=(&cellcnt>&DC_MAXCELLS_WEBEDIT)
,mac=&_program
,msg=Selection is too wide (&obscnt rows x &colcnt cols) - please filter and try again!
)
%let maxrows=&DC_MAXOBS_WEBEDIT;
%let limitvar=DC_MAXOBS_WEBEDIT;
%if &colcnt>0 %then %do;
%let cellrows=%sysfunc(floor(&DC_MAXCELLS_WEBEDIT/&colcnt));
%if %sysevalf(&cellrows lt &maxrows) %then %do;
%let maxrows=&cellrows;
%let limitvar=DC_MAXCELLS_WEBEDIT;
%end;
%end;
%if %sysevalf(&maxrows lt 1) %then %let maxrows=1;
%global truncated trunc_msg;
%let truncated=0;
%if %sysevalf(&obscnt gt &maxrows) %then %let truncated=1;
%let trunc_msg=;
%if &truncated=1 %then %let trunc_msg=Only the first &maxrows rows are loaded - the &limitvar limit has been reached. Filter the table to work with the rest.;
/* order pk fields at start of table. The delete flag is NOT added here:
@@ -297,9 +309,9 @@ select upcase(loadtype)
,Str2=&pk
);
%put sourcevars=&sourcevars;
/* the payload is capped here, at the row limit - the limits above are
checked against the whole selection, this is what goes to the browser */
options obs=&DC_MAXOBS_WEBEDIT;
/* the payload is capped here, at &maxrows - whichever of the row and cell
limits that works out to - this is what goes to the browser */
options obs=&maxrows;
data outdata;
/* pk fields come first */
%if %length(&pk)>0 %then %do;
@@ -572,7 +584,7 @@ select count(*) into: ismap from &mpelib..mpe_xlmap_info
where XLMAP_TARGETLIBDS="&orig_libds" and &dc_dttmtfmt. le TX_TO;
data sasparams;
length colHeaders $20000 filter_text $32767;
length colHeaders $20000 filter_text $32767 trunc_msg $200;
colHeaders=cats(upcase("%mf_getvarlist(sasdata1,dlm=%str(,))"));
pkCnt=countw("&pk");
pk="&pk";
@@ -583,6 +595,10 @@ data sasparams;
if trim(symget('rk_underlying')) ne '' then rk_flag=1;
else rk_flag=0;
filter_text=symget('filter_text');
/* set when the selection was cut down to the row / cell limit, so the EDIT
screen can say so against the table name */
truncated=&truncated;
trunc_msg="&trunc_msg";
if %mf_nobs(work.cls_rules)=0 then cls_flag=0;
else cls_flag=1;
put (_all_)(=);
+80 -48
View File
@@ -2,17 +2,19 @@
@file
@brief testing getdata row and cell limits
@details The EDIT screen refuses a selection that is too big on either
count - rows (DC_MAXOBS_WEBEDIT) or cells (DC_MAXCELLS_WEBEDIT). The row
check runs first, so a selection over the row limit reports "Table is too
big" and one under the row limit but over the cell limit reports
"Selection is too wide". Both aborts are proven by the child job state
(an aborted service registers no webout).
@details The EDIT screen is bounded by two limits - rows
(DC_MAXOBS_WEBEDIT) and cells, rows x columns (DC_MAXCELLS_WEBEDIT) -
whichever is reached first. A selection over either is SERVED TRUNCATED,
not refused: the grid gets the first rows that fit, and sasparams carries
TRUNCATED=1 plus a TRUNC_MSG naming the limit, which the EDIT screen shows
as a warning against the table name.
Fixture: dctest.wideboy (1000 rows x 1004 columns), created and registered
by testsetup. Filtered to 300 rows it is over the 250 row limit; filtered
to 200 rows it is under the row limit but 200 x 1004 = 200,800 cells, over
the 200,000 cell limit.
Test 1 uses MPE_X_TEST (several hundred rows, few columns) so the row
limit alone decides the cap. Test 2 uses the fixture dctest.wideboy (1000
rows x 1001 columns, created and registered by testsetup) filtered to 200
rows: that is under the row limit but 200 x 1001 = 200,200 cells, over the
200,000 cell limit, so the cell limit decides the cap -
floor(200000 / 1001) = 199 rows.
<h4> SAS Macros </h4>
@li mp_assert.sas
@@ -25,7 +27,7 @@
%let _program=&appLoc/services/editors/getdata;
/**
* build a ROW_ID <= &maxrow filter on dctest.wideboy, leaving the filter_rk
* Build a ROW_ID <= &maxrow filter on dctest.wideboy, leaving the filter_rk
* in &filter_rk
*/
%macro wideboy_filter(maxrow);
@@ -57,61 +59,91 @@
%mend wideboy_filter;
/**
* Test 1 - row limit (300 rows > 250)
* Call getdata as a child job and expose the webout as the &outlib JSON
* libname, so the test can read what the browser would have received.
*/
%wideboy_filter(300)
%let f1=%mf_getuniquefileref();
data _null_;
file &f1 termstr=crlf;
put 'LIBDS:$43. FILTER_RK:8.';
put "dctest.wideboy,&filter_rk";
run;
%macro call_getdata(libds,filter_rk,outlib);
%let fref=%mf_getuniquefileref();
data _null_;
file &fref termstr=crlf;
put 'LIBDS:$43. FILTER_RK:8.';
put "&libds,&filter_rk";
run;
%mx_execute(&_program,
viyacontext=&defaultcontext,
inputfiles=&fref:sascontroltable,
outlib=&outlib
)
%let syscc=0;
%mend call_getdata;
%mx_execute(&_program,
viyacontext=&defaultcontext,
inputfiles=&f1:sascontroltable,
outref=web1
)
/**
* Test 1 - row limit. MPE_X_TEST is well under the cell limit, so the
* 250 row default is what caps the payload.
*/
%call_getdata(&dclib..MPE_X_TEST,-1,web1)
%let abort1=0;
%let t1_nobs=-1;
%let t1_trunc=-1;
%let t1_msg=;
proc sql noprint;
select count(*) into: t1_nobs from web1.sasdata;
quit;
data _null_;
set work.results;
if state='canceled' then call symputx('abort1',1);
set web1.sasparams;
call symputx('t1_trunc',truncated);
call symputx('t1_msg',trunc_msg);
putlog (_all_)(=);
run;
%let syscc=0;
%mp_assert(
iftrue=(&abort1=1),
desc=Test 1 - a selection over the row limit is refused,
iftrue=(&t1_trunc=1),
desc=Test 1 - a selection over the row limit is served, flagged as truncated,
outds=work.test_results
)
%mp_assert(
iftrue=(&t1_nobs=250),
desc=Test 1 - the payload is capped at DC_MAXOBS_WEBEDIT (250) rows,
outds=work.test_results
)
%mp_assert(
iftrue=(%index(&t1_msg,DC_MAXOBS_WEBEDIT)>0),
desc=Test 1 - the truncation message names the DC_MAXOBS_WEBEDIT limit,
outds=work.test_results
)
/**
* Test 2 - cell limit (200 rows x 1004 cols = 200,800 > 200,000)
* Test 2 - cell limit (200 rows x 1001 cols = 200,200 > 200,000)
*/
%wideboy_filter(200)
%let f2=%mf_getuniquefileref();
data _null_;
file &f2 termstr=crlf;
put 'LIBDS:$43. FILTER_RK:8.';
put "dctest.wideboy,&filter_rk";
run;
%call_getdata(dctest.wideboy,&filter_rk,web2)
%mx_execute(&_program,
viyacontext=&defaultcontext,
inputfiles=&f2:sascontroltable,
outref=web2
)
%let abort2=0;
%let t2_nobs=-1;
%let t2_trunc=-1;
%let t2_msg=;
proc sql noprint;
select count(*) into: t2_nobs from web2.sasdata;
quit;
data _null_;
set work.results;
if state='canceled' then call symputx('abort2',1);
set web2.sasparams;
call symputx('t2_trunc',truncated);
call symputx('t2_msg',trunc_msg);
putlog (_all_)(=);
run;
%mp_assert(
iftrue=(&abort2=1),
desc=Test 2 - a selection over the cell limit is refused,
iftrue=(&t2_trunc=1),
desc=Test 2 - a selection over the cell limit is served, flagged as truncated,
outds=work.test_results
)
%mp_assert(
iftrue=(&t2_nobs=199),
desc=Test 2 - the payload is capped at floor(DC_MAXCELLS_WEBEDIT / cols),
outds=work.test_results
)
%mp_assert(
iftrue=(%index(&t2_msg,DC_MAXCELLS_WEBEDIT)>0),
desc=Test 2 - the truncation message names the DC_MAXCELLS_WEBEDIT limit,
outds=work.test_results
)
@@ -21,10 +21,10 @@
editing MPE_GROUPS alone - Row Level Security is skipped for them - so the
test reports a skip row instead of running the assertions.
The call passes a filter (PRIMARY_KEY_FIELD < 100) because getdata.sas also
enforces the row / cell limits, and the unfiltered MPE_X_TEST is above the
row limit (642 rows against DC_MAXOBS_WEBEDIT:250) - the limits test
(getdata.test.1.sas) covers that abort separately.
The call passes a filter (PRIMARY_KEY_FIELD < 100) so the whole selection is
served - the unfiltered MPE_X_TEST is above the row limit (642 rows against
DC_MAXOBS_WEBEDIT:250) and would come back truncated. The limits test
(getdata.test.1.sas) covers that truncation separately.
<h4> SAS Macros </h4>
@li mf_getuniquefileref.sas