fix(editor): resolve a primary key's live formula to its computed value, not the raw formula text
Build / Build-and-ng-test (pull_request) Successful in 5m12s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m40s
Build / Build-and-test-development (pull_request) Successful in 27m13s

This commit is contained in:
YuryShkoda
2026-08-26 19:08:57 +03:00
parent 2cc8489892
commit d72e19a308
5 changed files with 589 additions and 0 deletions
+117
View File
@@ -1652,6 +1652,123 @@ context('editor tests: ', function () {
})
})
})
// A primary key identifies its own row for the rest of the edit session
// (dataModified/classifyRow match rows by PK) - a live, still-
// recalculating formula left sitting in a PK cell would let that
// identity drift between when it's entered and when submission finally
// resolves it, silently dropping the row from what's submitted. So
// unlike B_COL above, a formula pasted into PRIMARY_KEY_FIELD resolves
// to its computed value immediately, the moment the paste completes,
// rather than staying live until submit. Row 4 (PK=5): pasting
// `=999+1` evaluates to 1000, a value clearly different from both the
// original and every other row's PK, so a passing assertion here can't
// be a coincidence (nor a PK collision with another seeded row).
it('51 | Resolves a formula pasted into the primary key column to its computed value immediately', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_test')
clickOnEdit(() => {
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
timeout: longerCommandTimeout
}).then(() => {
cy.intercept('POST', '**/SASjsApi/stp/execute*', (req) => {
const bodyText = JSON.stringify(req.body || '')
if (bodyText.includes('stagedata')) {
expect(bodyText).to.include('"PRIMARY_KEY_FIELD":1000')
}
req.continue()
}).as('stpExecute')
getCellByHeaderAndRow(4, 'PRIMARY_KEY_FIELD').should('have.text', '5')
getCellByHeaderAndRow(4, 'PRIMARY_KEY_FIELD').click()
pasteTextIntoFocusedCell('=999+1')
getCellByHeaderAndRow(4, 'PRIMARY_KEY_FIELD').should(
'have.text',
'1000'
)
submitTable()
cy.get('#submitBtn', { timeout: longerCommandTimeout })
.should('exist')
.should('not.be.disabled')
cy.get('#formFields_8').type(
'primary key computed formula value submission test'
)
submitTableMessage()
})
})
})
// Nothing gates a HARDFORMULA/SOFTFORMULA rule's BASE_COL by column type
// or PK-ness - MPE_X_FORMULA_PK_TEST's composite PK is itself made up of
// a HARDFORMULA numeric column and a SOFTFORMULA character column. Left
// unresolved, that reproduces the same PK-identity bug test 51 fixes -
// just triggered by the rule seeding instead of a user edit, and present
// on every row from the very first render rather than only an edited
// one. resolvePkFormulaSeedValues (editor.component.ts) freezes both PK
// cells to their computed values once, right after the initial table
// load, so they display correctly immediately and the row still submits
// - editing A_COL afterward (which the PK formulas reference) must NOT
// retroactively recompute either PK cell, proving they're frozen rather
// than still-live.
it('52 | A primary key made of HARDFORMULA/SOFTFORMULA columns freezes to its computed value at load and still submits correctly', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_formula_pk_test')
// Row 0: A_COL=2, B_COL=3 -> PK_HARDFORMULA_COL=6, PK_SOFTFORMULA_COL="PK-2".
getCellByHeaderAndRow(0, 'PK_HARDFORMULA_COL').should('have.text', '6')
getCellByHeaderAndRow(0, 'PK_SOFTFORMULA_COL').should('have.text', 'PK-2')
clickOnEdit(() => {
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
timeout: longerCommandTimeout
}).then(() => {
cy.intercept('POST', '**/SASjsApi/stp/execute*', (req) => {
const bodyText = JSON.stringify(req.body || '')
if (bodyText.includes('stagedata')) {
// The row must not be silently dropped from what's submitted
// - both frozen PK values and the actually-edited column must
// be present.
expect(bodyText).to.include('"PK_HARDFORMULA_COL":6')
expect(bodyText).to.include('"PK_SOFTFORMULA_COL":"PK-2"')
expect(bodyText).to.include('"A_COL":9')
}
req.continue()
}).as('stpExecute')
getCellByHeaderAndRow(0, 'A_COL')
.dblclick({ force: true })
.then(() => {
cy.focused().clear().type('9{enter}')
})
// Frozen, not live - A_COL changing from 2 to 9 would recompute
// PK_HARDFORMULA_COL to 27 if it were still a live formula.
getCellByHeaderAndRow(0, 'PK_HARDFORMULA_COL').should('have.text', '6')
submitTable()
cy.get('#submitBtn', { timeout: longerCommandTimeout })
.should('exist')
.should('not.be.disabled')
cy.get('#formFields_8').type(
'formula-driven composite primary key submission test'
)
submitTableMessage()
})
})
})
})
// Handsontable virtualizes columns — with 18 columns on MPE_X_NEW, only
+83
View File
@@ -1846,6 +1846,53 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
/**
* A primary key identifies its own row for the rest of the edit session
* (getRowKey, dataModified, classifyRow all match rows by comparing PK
* values) - a HARDFORMULA/SOFTFORMULA rule on a PK column gets seeded by
* applyFormulaRules as a live `=...` formula string exactly like any
* other formula column, but a PK can never be allowed to keep
* recalculating for the rest of the session the way a normal formula
* column does: saveTable's own formulaRules loop only resolves a formula
* column's computed value once, at submit, so anything that changed a
* column the PK's formula depends on in between would silently shift the
* PK out from under dataModified/classifyRow's PK-matching - the same
* failure mode the afterChange hook below already prevents for a
* user-typed PK formula. So a PK-as-formula-column is frozen to its
* computed value once, right here at load. Both dataSource and
* dataSourceUnchanged get the same frozen value - overriding whatever
* overlayFormulaRawValuesOnUnchanged set for it, since a PK's identity
* role takes priority over the normal "did a formula overwrite real
* data" modified-detection every other formula column gets.
*/
private resolvePkFormulaSeedValues(): void {
const hot = this.hotInstance
const formulaBaseCols = getFormulaColumnNames(
this.dcValidator?.getDqDetails() ?? []
)
const pkFormulaCols = this.headerPks.filter((pk) =>
formulaBaseCols.includes(pk)
)
if (pkFormulaCols.length === 0) return
// Deferred for the same reason as this method's own caller
// (markOverwrittenCells) defers its setDataAtRowProp call above: right
// after the initial hot.updateSettings(), the Formulas plugin's
// hidden-column index mapping hasn't settled yet, and setDataAtRowProp
// throws ExpectedValueOfTypeError before it has.
setTimeout(() => {
this.dataSource.forEach((_row, rowIndex) => {
for (const pkCol of pkFormulaCols) {
const computed = hot.getDataAtRowProp(rowIndex, pkCol)
hot.setDataAtRowProp(rowIndex, pkCol, computed, 'resolvePkFormula')
const unchangedRow = this.dataSourceUnchanged?.[rowIndex]
if (unchangedRow) unchangedRow[pkCol] = computed
}
})
}, 0)
}
/**
* Recomputes this row's classification (M/A/D/U) and writes it into the
* EDIT_STATUS cell - the same classification the row-header symbol shows,
@@ -4144,6 +4191,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
// actually evaluated them against the data/formulas settings just
// applied.
this.markOverwrittenCells()
this.resolvePkFormulaSeedValues()
this.hotTable.hidden = false
// Keep the context menu enabled in view mode too so Copy/Export remain
@@ -4448,6 +4496,41 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
return value
})
// A primary key identifies its own row for the rest of the edit session
// (getRowKey, dataModified, classifyRow all match rows by comparing PK
// values) - if it's left holding a live formula, HyperFormula's
// recalculation can shift that identity out from under those matches
// between whenever the formula was entered and whenever saveTable()
// finally resolves it. Unlike any other column, resolve a PK's formula
// to its computed value immediately, right when it's entered (typed,
// pasted, autofilled, or "Apply as formula"'d), so the PK is never
// anything other than a stable, already-computed value going forward.
// Deferred via setTimeout, same as the NOTNULL default-population hook
// above, since writing back through setDataAtRowProp from inside
// afterChange itself would re-enter change processing immediately.
hot.addHook('afterChange', (changes: any[], source: any) => {
if (!changes || source === 'loadData' || source === 'resolvePkFormula')
return
for (const change of changes) {
if (!change) continue
const [row, prop, , newValue] = change
if (typeof newValue !== 'string' || !newValue.startsWith('=')) continue
const colName =
typeof prop === 'string'
? prop
: (hot.colToProp(prop as number) as string)
if (!this.headerPks.includes(colName)) continue
setTimeout(() => {
const computed = hot.getDataAtRowProp(row, colName)
hot.setDataAtRowProp(row, colName, computed, 'resolvePkFormula')
}, 0)
}
})
hot.addHook('beforePaste', (data: any, cords: any) => {
const startCol = cords[0].startCol
const startRow = cords[0].startRow
@@ -689,6 +689,148 @@ describe('submission resolves a genuine live formula to its computed value, not
expect(dataSource[0].B_COL).toEqual(4)
})
// HARDFORMULA/SOFTFORMULA rules aren't restricted to character columns
// either - applyFormulaRules seeds the same live `=...` string into a
// numeric column regardless of RULE_TYPE, and this resolve step (the
// same one above) doesn't distinguish HARDFORMULA from SOFTFORMULA or
// ad-hoc from rule-seeded - it just resolves whatever `=`-led text it
// finds. A non-PK column is never used for row-matching, so this is
// already the correct, complete fix - unlike a PK-as-formula-column
// (see the freeze-at-load describe block below).
it('submits the computed value for a HARDFORMULA-seeded numeric (non-PK) column', () => {
const dataSource: any[] = [{ PRIMARY_KEY_FIELD: 1, B_COL: '=3+3' }]
runSubmitPrep(dataSource, ['PRIMARY_KEY_FIELD', 'B_COL'], new Map())
expect(dataSource[0].B_COL).toEqual(6)
})
it('submits the computed value for a SOFTFORMULA-seeded numeric (non-PK) column', () => {
const dataSource: any[] = [{ PRIMARY_KEY_FIELD: 1, B_COL: '=4+4' }]
runSubmitPrep(dataSource, ['PRIMARY_KEY_FIELD', 'B_COL'], new Map())
expect(dataSource[0].B_COL).toEqual(8)
})
})
/**
* A primary key identifies its own row for the rest of the edit session -
* getRowKey, dataModified (see saveTable) and classifyRow all match a row
* up by comparing its current PK value against an earlier snapshot. A live
* formula left sitting in a PK cell keeps recalculating right up until
* submission resolves it, so the PK that gets matched against an earlier
* snapshot can differ from the PK the row was snapshotted under -
* silently dropping the row from what's submitted. Unlike any other
* column (which stays live and is resolved only once, at submit time), a
* PK cell resolves its formula to a plain computed value the moment that
* edit completes - typed, pasted, autofilled, or "Apply as formula"'d -
* exactly like editor.component.ts's own dedicated afterChange hook.
* Mirrors that hook's resolve step against a real
* Handsontable + HyperFormula instance, since proving a formula's
* computed value requires a real engine.
*/
describe('a primary key resolves a live formula to its computed value as soon as editing completes', () => {
const headerPks = ['PRIMARY_KEY_FIELD']
// Mirrors the afterChange hook's own resolve step, minus the setTimeout
// deferral (an implementation detail there purely to avoid re-entering
// change processing synchronously from inside afterChange itself - the
// resolve logic it defers to is exactly this).
const resolvePkFormulaIfNeeded = (
hot: Handsontable,
row: number,
colName: string,
newValue: any
): void => {
if (typeof newValue !== 'string' || !newValue.startsWith('=')) return
if (!headerPks.includes(colName)) return
const computed = hot.getDataAtRowProp(row, colName)
hot.setDataAtRowProp(row, colName, computed, 'resolvePkFormula')
}
const setup = (dataSource: any[], colNames: string[]): Handsontable => {
const hot = new Handsontable(document.createElement('div'), {
data: dataSource,
columns: colNames.map((c) => ({ data: c })),
formulas: { engine: HyperFormula, licenseKey: 'gpl-v3' },
licenseKey: 'non-commercial-and-evaluation'
})
hot.render()
return hot
}
it('resolves a formula typed/pasted/autofilled into a PK cell to its computed value once editing completes', () => {
const dataSource: any[] = [{ PRIMARY_KEY_FIELD: 1, PLAIN_TEXT_COL: 'x' }]
const hot = setup(dataSource, ['PRIMARY_KEY_FIELD', 'PLAIN_TEXT_COL'])
hot.setDataAtRowProp(0, 'PRIMARY_KEY_FIELD', '=2+3')
resolvePkFormulaIfNeeded(hot, 0, 'PRIMARY_KEY_FIELD', '=2+3')
expect(dataSource[0].PRIMARY_KEY_FIELD).toEqual(5)
hot.destroy()
})
// The resolve step never checks the PK's own DDTYPE - only headerPks
// membership - so a character-typed PK whose formula resolves to text
// (not a number) is frozen exactly the same way as a numeric one.
it('resolves a formula into a character-typed PK cell to its computed text value once editing completes', () => {
const dataSource: any[] = [
{ PRIMARY_KEY_FIELD: 'PK-1', PLAIN_TEXT_COL: 'x' }
]
const hot = setup(dataSource, ['PRIMARY_KEY_FIELD', 'PLAIN_TEXT_COL'])
hot.setDataAtRowProp(0, 'PRIMARY_KEY_FIELD', '="PK-" & 2')
resolvePkFormulaIfNeeded(hot, 0, 'PRIMARY_KEY_FIELD', '="PK-" & 2')
expect(dataSource[0].PRIMARY_KEY_FIELD).toEqual('PK-2')
hot.destroy()
})
it('leaves a non-PK column formula alone - stays live, resolved only at submission like before', () => {
const dataSource: any[] = [{ PRIMARY_KEY_FIELD: 1, PLAIN_TEXT_COL: 'x' }]
const hot = setup(dataSource, ['PRIMARY_KEY_FIELD', 'PLAIN_TEXT_COL'])
hot.setDataAtRowProp(0, 'PLAIN_TEXT_COL', '=2+3')
resolvePkFormulaIfNeeded(hot, 0, 'PLAIN_TEXT_COL', '=2+3')
expect(dataSource[0].PLAIN_TEXT_COL).toEqual('=2+3')
hot.destroy()
})
it('leaves a genuinely numeric edit into a PK column untouched - not treated as a formula', () => {
const dataSource: any[] = [{ PRIMARY_KEY_FIELD: 1, PLAIN_TEXT_COL: 'x' }]
const hot = setup(dataSource, ['PRIMARY_KEY_FIELD', 'PLAIN_TEXT_COL'])
hot.setDataAtRowProp(0, 'PRIMARY_KEY_FIELD', 5)
resolvePkFormulaIfNeeded(hot, 0, 'PRIMARY_KEY_FIELD', 5)
expect(dataSource[0].PRIMARY_KEY_FIELD).toEqual(5)
hot.destroy()
})
it('a PK resolved eagerly on entry stays matchable against an earlier dataModified snapshot at submit time', () => {
// The PK is already a plain computed value by the time anything
// snapshots it, so dataModified's PK-matching never sees it change
// out from under itself.
const dataSource: any[] = [{ PRIMARY_KEY_FIELD: 1, PLAIN_TEXT_COL: 'x' }]
const hot = setup(dataSource, ['PRIMARY_KEY_FIELD', 'PLAIN_TEXT_COL'])
hot.setDataAtRowProp(0, 'PRIMARY_KEY_FIELD', '=2+3')
resolvePkFormulaIfNeeded(hot, 0, 'PRIMARY_KEY_FIELD', '=2+3')
const dataModified: any[] = [
{ PRIMARY_KEY_FIELD: dataSource[0].PRIMARY_KEY_FIELD }
]
const stillMatches = dataModified.some((row) =>
headerPks.every((pk) => row[pk] === dataSource[0][pk])
)
expect(stillMatches).toEqual(true)
hot.destroy()
})
})
const submitStrip = (
@@ -705,3 +847,102 @@ const submitStrip = (
}
}
}
/**
* Nothing gates a HARDFORMULA/SOFTFORMULA rule's BASE_COL by column type or
* PK-ness, so a primary key can itself be one - applyFormulaRules seeds it
* with a live `=...` formula string at load, exactly like any other
* formula column. Left alone, that reproduces the exact bug the
* user-entered-PK-formula fix above solves, just triggered by the rule
* seeding instead of a user edit: saveTable's formulaRules loop only
* resolves it once, at submit, so anything that changed a column the PK's
* formula depends on in between would silently shift dataModified's
* PK-matching out from under itself - and, unlike a user-entered formula,
* a rule-seeded one is ALWAYS present in dataSource, from the very first
* render, so it's not a rare edge case at all. resolvePkFormulaSeedValues
* (editor.component.ts, called once right after the initial
* hot.updateSettings()) freezes it to its computed value immediately,
* mirrored here against a real Handsontable + HyperFormula instance -
* writing the same frozen value onto both dataSource and
* dataSourceUnchanged (deliberately overriding whatever
* overlayFormulaRawValuesOnUnchanged put there), since a PK must agree
* with itself on both sides for classifyRow's own PK-based lookup to ever
* find its row again.
*/
describe('a primary key configured as a HARDFORMULA/SOFTFORMULA column freezes to its computed value at load', () => {
const resolvePkFormulaSeedValues = (
hot: Handsontable,
dataSource: any[],
dataSourceUnchanged: any[],
pkFormulaCols: string[]
): void => {
dataSource.forEach((_row, rowIndex) => {
for (const pkCol of pkFormulaCols) {
const computed = hot.getDataAtRowProp(rowIndex, pkCol)
hot.setDataAtRowProp(rowIndex, pkCol, computed, 'resolvePkFormula')
const unchangedRow = dataSourceUnchanged[rowIndex]
if (unchangedRow) unchangedRow[pkCol] = computed
}
})
}
it('freezes a HARDFORMULA-driven numeric PK to its computed value on both dataSource and dataSourceUnchanged', () => {
// applyFormulaRules seeds dataSource and dataSourceUnchanged
// identically at load - before anything has diverged them yet.
const dataSource: any[] = [{ PRIMARY_KEY_FIELD: '=2+3', A_COL: 1 }]
const dataSourceUnchanged: any[] = [{ PRIMARY_KEY_FIELD: '=2+3', A_COL: 1 }]
const hot = new Handsontable(document.createElement('div'), {
data: dataSource,
columns: ['PRIMARY_KEY_FIELD', 'A_COL'].map((c) => ({ data: c })),
formulas: { engine: HyperFormula, licenseKey: 'gpl-v3' },
licenseKey: 'non-commercial-and-evaluation'
})
hot.render()
resolvePkFormulaSeedValues(hot, dataSource, dataSourceUnchanged, [
'PRIMARY_KEY_FIELD'
])
expect(dataSource[0].PRIMARY_KEY_FIELD).toEqual(5)
expect(dataSourceUnchanged[0].PRIMARY_KEY_FIELD).toEqual(5)
// classifyRow's own PK-based lookup - both sides now agree.
const found = dataSourceUnchanged.find((row) =>
['PRIMARY_KEY_FIELD'].every((pk) => row[pk] === dataSource[0][pk])
)
expect(found).toBeTruthy()
hot.destroy()
})
it('freezes a SOFTFORMULA-driven character PK to its computed value on both dataSource and dataSourceUnchanged', () => {
const dataSource: any[] = [{ PRIMARY_KEY_FIELD: '="PK-" & 2', A_COL: 1 }]
const dataSourceUnchanged: any[] = [
{ PRIMARY_KEY_FIELD: '="PK-" & 2', A_COL: 1 }
]
const hot = new Handsontable(document.createElement('div'), {
data: dataSource,
columns: ['PRIMARY_KEY_FIELD', 'A_COL'].map((c) => ({ data: c })),
formulas: { engine: HyperFormula, licenseKey: 'gpl-v3' },
licenseKey: 'non-commercial-and-evaluation'
})
hot.render()
resolvePkFormulaSeedValues(hot, dataSource, dataSourceUnchanged, [
'PRIMARY_KEY_FIELD'
])
expect(dataSource[0].PRIMARY_KEY_FIELD).toEqual('PK-2')
expect(dataSourceUnchanged[0].PRIMARY_KEY_FIELD).toEqual('PK-2')
const found = dataSourceUnchanged.find((row) =>
['PRIMARY_KEY_FIELD'].every((pk) => row[pk] === dataSource[0][pk])
)
expect(found).toBeTruthy()
hot.destroy()
})
})
+144
View File
@@ -1764,6 +1764,148 @@ let webouts = {
SYSWARNINGTEXT: "",
END_DTTM: "2026-07-28T12:00:00.000000",
MEMSIZE: "1MB"
},
// Nothing gates a HARDFORMULA/SOFTFORMULA rule's BASE_COL by column
// type or PK-ness - this table's composite PK is itself made up of
// two formula-driven columns (a HARDFORMULA numeric one, a
// SOFTFORMULA character one), isolating that a PK-as-formula-column
// freezes to its computed value at load and the row still submits
// correctly, rather than silently dropping out (see
// resolvePkFormulaSeedValues in editor.component.ts).
MPE_X_FORMULA_PK_TEST: {
SYSDATE: "26AUG26",
SYSTIME: "12:00",
approvers: [],
cols: [
{
NAME: "PK_HARDFORMULA_COL",
LABEL: "PK_HARDFORMULA_COL",
FMTNAME: "",
DDTYPE: "N",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "HARDFORMULA: computed A_COL * B_COL, also this table's first PK column",
LONGDESC: "",
COLTYPE: "{\"data\":\"PK_HARDFORMULA_COL\",\"type\":\"numeric\",\"format\":\"0\"}"
},
{
NAME: "PK_SOFTFORMULA_COL",
LABEL: "PK_SOFTFORMULA_COL",
FMTNAME: "",
DDTYPE: "C",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "SOFTFORMULA: computed \"PK-\" & A_COL, also this table's second PK column",
LONGDESC: "",
COLTYPE: "{\"data\":\"PK_SOFTFORMULA_COL\"}"
},
{
NAME: "A_COL",
LABEL: "A_COL",
FMTNAME: "",
DDTYPE: "N",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: "",
COLTYPE: "{\"data\":\"A_COL\",\"type\":\"numeric\",\"format\":\"0\"}"
},
{
NAME: "B_COL",
LABEL: "B_COL",
FMTNAME: "",
DDTYPE: "N",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: "",
COLTYPE: "{\"data\":\"B_COL\",\"type\":\"numeric\",\"format\":\"0\"}"
}
],
dqdata: [],
dqrules: [
{ BASE_COL: "PK_HARDFORMULA_COL", RULE_TYPE: "HARDFORMULA", RULE_VALUE: "=A_COL * B_COL" },
{ BASE_COL: "PK_SOFTFORMULA_COL", RULE_TYPE: "SOFTFORMULA", RULE_VALUE: "=\"PK-\" & A_COL" }
],
dsmeta: [
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Set Name", VALUE: "MPE_X_FORMULA_PK_TEST" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Member Type", VALUE: "DATA" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Engine", VALUE: "V9" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Observations", VALUE: "2" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Variables", VALUE: "4" }
],
maxvarlengths: [
{ NAME: "_____DELETE__THIS__RECORD_____", MAXLEN: 3 },
{ NAME: "pk_hardformula_col", MAXLEN: 8 },
{ NAME: "pk_softformula_col", MAXLEN: 128 },
{ NAME: "a_col", MAXLEN: 8 },
{ NAME: "b_col", MAXLEN: 8 }
],
query: [],
// Row 1: A_COL=2, B_COL=3 -> PK_HARDFORMULA_COL=6, PK_SOFTFORMULA_COL="PK-2".
// Row 2: A_COL=5, B_COL=4 -> PK_HARDFORMULA_COL=20, PK_SOFTFORMULA_COL="PK-5".
// Both PK columns' seed values below are irrelevant - applyFormulaRules
// unconditionally overwrites every formula-rule column at load, same as
// MPE_X_FORMULA_TEST's own FORMULA_HARD_COL/FORMULA_SOFT_COL.
sasdata: [
{
_____DELETE__THIS__RECORD_____: "No",
PK_HARDFORMULA_COL: "",
PK_SOFTFORMULA_COL: "",
A_COL: 2,
B_COL: 3
},
{
_____DELETE__THIS__RECORD_____: "No",
PK_HARDFORMULA_COL: "",
PK_SOFTFORMULA_COL: "",
A_COL: 5,
B_COL: 4
}
],
$sasdata: {
vars: {
_____DELETE__THIS__RECORD_____: { format: "$3.", label: "_____DELETE__THIS__RECORD_____", length: "3", type: "char" },
PK_HARDFORMULA_COL: { format: "best.", label: "PK_HARDFORMULA_COL", length: "8", type: "num" },
PK_SOFTFORMULA_COL: { format: "$128.", label: "PK_SOFTFORMULA_COL", length: "128", type: "char" },
A_COL: { format: "best.", label: "A_COL", length: "8", type: "num" },
B_COL: { format: "best.", label: "B_COL", length: "8", type: "num" }
}
},
sasparams: [
{
COLHEADERS: "_____DELETE__THIS__RECORD_____,PK_HARDFORMULA_COL,PK_SOFTFORMULA_COL,A_COL,B_COL",
FILTER_TEXT: "",
PKCNT: 2,
PK: "PK_HARDFORMULA_COL PK_SOFTFORMULA_COL",
DTVARS: "",
DTTMVARS: "",
TMVARS: "",
LOADTYPE: "UPDATE",
RK_FLAG: 0,
CLS_FLAG: 0
}
],
xl_rules: [],
_DEBUG: "",
_PROGRAM: "/Public/app/dc/services/editors/getdata",
AUTOEXEC: "",
MF_GETUSER: "sasdemo",
SYSCC: "0",
SYSENCODING: "utf-8",
SYSERRORTEXT: "",
SYSHOSTNAME: "SAS",
SYSPROCESSID: "0",
SYSPROCESSMODE: "SAS Batch Mode",
SYSPROCESSNAME: "",
SYSJOBID: "1",
SYSSCPL: "Linux",
SYSSITE: "123",
SYSUSERID: "sasjssrv",
SYSVLONG: "9.04.01M7P080520",
SYSWARNINGTEXT: "",
END_DTTM: "2026-08-26T12:00:00.000000",
MEMSIZE: "1MB"
}
}
@@ -1802,6 +1944,8 @@ if (_WEBIN_FILEREF1) {
if (file1.includes('MPE_X_NEW')) {
table = 'MPE_X_NEW'
} else if (file1.includes('MPE_X_FORMULA_PK_TEST')) {
table = 'MPE_X_FORMULA_PK_TEST'
} else if (file1.includes('MPE_X_FORMULA_TEST')) {
table = 'MPE_X_FORMULA_TEST'
} else if (file1.includes('MPE_X_TEST')) {
@@ -59,6 +59,10 @@ _webout = `{"SYSDATE" : "26SEP22"
"LIBREF": "DC996664",
"DSN": "MPE_X_FORMULA_TEST"
},
{
"LIBREF": "DC996664",
"DSN": "MPE_X_FORMULA_PK_TEST"
},
{
"LIBREF": "DC996664",
"DSN": "MPE_DATADICTIONARY"