Author SHA1 Message Date
blog-dev b6fea27963 feat: surface Cyber Essentials certification on homepage hero and footer 2026-09-11 19:45:06 +00:00
blog-dev aa1f015f5a docs(blog): punchier LinkedIn draft for non-DC users - pain-first, no jargon
publish / Build-and-publish (push) Successful in 6m39s
2026-09-04 09:29:20 +00:00
blog-dev 79b62e4cfc docs(blog): add source LinkedIn post comment for the v7.13 article
publish / Build-and-publish (push) Successful in 6m33s
2026-09-04 08:06:46 +00:00
blog-dev 73d4796638 fix(blog): show sasdemo instead of root user in audit demo screenshot
publish / Build-and-publish (push) Successful in 6m36s
2026-09-04 07:51:32 +00:00
blog-dev 7267af58c4 ci: drop npm cache from setup-node - save/restore costs exceed the benefit at this repo size
publish / Build-and-publish (push) Successful in 7m0s
2026-09-04 07:23:58 +00:00
blog-dev e5dfcd84da feat(blog): add audit-column demo image showing the IF formula resolving live
publish / Build-and-publish (push) Successful in 13m17s
2026-09-04 01:28:42 +00:00
blog-dev 02295334b3 fix(blog): restore hardening and polishing phrasing in v7.13 post
publish / Build-and-publish (push) Successful in 13m11s
2026-09-04 01:09:48 +00:00
blog-dev e7a538ed9a feat(blog): link getdata and postedit hook to code.datacontroller.io
publish / Build-and-publish (push) Successful in 15m42s
2026-09-04 00:53:20 +00:00
blog-dev 60a19dda89 fix(blog): correct PK live-formula wording in v7.13 post
publish / Build-and-publish (push) Successful in 15m16s
2026-09-04 00:36:58 +00:00
blog-dev b415793eec fix(blog): trim intra-release fix churn from the also-in-release list
publish / Build-and-publish (push) Successful in 15m40s
2026-09-04 00:20:36 +00:00
blog-dev d352ea8fea fix(blog): re-encode v7.13 images to bust poisoned CDN edge cache entries
publish / Build-and-publish (push) Successful in 15m47s
The /static/* path sits behind a Cloudflare cache-everything rule with a
62-day edge TTL. An image fetched mid-deploy can pin a bad (truncated/404)
response at an edge POP. Re-encoding produces new content-hashed URLs so
every variant is fetched fresh from origin. Pixels are identical.
2026-09-04 00:19:32 +00:00
blog-dev 9e04cd4220 feat(blog): v7.13 release post - formulas & regex with live screenshots
publish / Build-and-publish (push) Successful in 15m47s
2026-09-03 23:56:29 +00:00
blog-dev 181c3ae834 fix(feed): update LinkedIn source copy - community edition, not 5-user free tier
publish / Build-and-publish (push) Successful in 13m5s
2026-08-27 17:04:13 +01:00
blog-dev 0ec0789ac4 feat(feed): five lines of defence post with cover image
publish / Build-and-publish (push) Successful in 13m1s
2026-08-27 16:43:07 +01:00
blog-dev 2e5fc4a368 feat(feed): legacy data ingestion post with cover image
publish / Build-and-publish (push) Successful in 12m56s
2026-08-24 16:37:11 +01:00
blog-dev 3d66f91820 fix(feed): correct docs link to /restore/
publish / Build-and-publish (push) Successful in 12m54s
2026-08-20 22:26:03 +01:00
blog-dev 5cdfadf75c feat(feed): use ctrl-z as cover, move trains meme into post body
publish / Build-and-publish (push) Successful in 13m0s
2026-08-20 22:20:56 +01:00
hermes fef2c3c12e chore: li update
publish / Build-and-publish (push) Successful in 12m58s
2026-08-20 22:14:47 +01:00
blog-dev b50efc01e2 feat(feed): expand rollback post LinkedIn copy, drop version refs
publish / Build-and-publish (push) Successful in 12m52s
2026-08-20 21:50:27 +01:00
blog-dev de23966721 feat(feed): add rollback data changes post with meme 2026-08-20 21:06:43 +01:00
17 changed files with 456 additions and 2 deletions

No files matched your search

-1
View File
@@ -14,7 +14,6 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci --legacy-peer-deps run: npm ci --legacy-peer-deps
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,199 @@
---
title: "v7.13 Release: Formulas & Regex"
description: Data Controller 7.13 wires a spreadsheet-grade formula engine into the editor grid, completing the point-of-entry validation story that began with 7.12's regex rules. Both are pure MPE_VALIDATIONS configuration - no code, no deployment.
date: '2026-09-03 12:00:00'
author: 'Allan Bowe'
authorLink: https://www.linkedin.com/in/allanbowe/
previewImg: './v713_cover.png'
tags:
- Releases
- Data Controller
---
# v7.13: Formulas & Regex
We stopped writing release-specific blog posts after v6.1 - the automated, public release system made them redundant for routine version bumps. But every so often a release lands that changes what the product can actually _do_, and v7.13 is one of them. Together with 7.12 it completes a piece of the [roadmap](https://docs.datacontroller.io/roadmap/) we have been chipping away at for a while: **frontend formulae and regex rules**, both configurable purely in `MPE_VALIDATIONS`.
If you configure data entry in Data Controller, this post is a practical guide: how the rules work, how to set them up, and the process flow from config to grid. All the screenshots below are captured from a live editor session against the demo tables, so what you see is what shipped.
## Why this matters
Data Controller's job is to let business users change data safely. A big part of "safely" is catching problems at the point of entry - rather than after an approval, in a batch log, or (worst case) in a report. The [validations](https://docs.datacontroller.io/dcc-validations/) framework already covered length, type, nullability, primary keys, ranges, casing and dropdowns. Two things were missing:
* **Computed values.** Plenty of tables have columns that are derived from other columns - a revenue column that is price times volume, a stamp column that records who last touched a row. Until now the options were a backend hook script (real SAS code to write, test and deploy) or just letting users type anything.
* **Pattern enforcement.** A dropdown is overkill when all you need is "this value looks like an email address" or "this postcode is well-formed". You want the _shape_ of the value checked as typed - and sometimes you want a hard block, sometimes just a gentle warning.
7.12 delivered regex rules (`HARDREGEX` / `SOFTREGEX`). 7.13 delivers formulas (`HARDFORMULA` / `SOFTFORMULA`). Both are rows in a config table. That is the whole feature.
## The process flow: from MPE_VALIDATIONS to the grid
Every configurable rule in Data Controller follows the same pipeline, and the new rules plug straight into it:
1. **Configure.** `MPE_VALIDATIONS` is itself a Data Controller table - open it from the navigation tree like any other, and add a row with `BASE_LIB`, `BASE_DS` and `BASE_COL` pointing at the column you want to govern, `RULE_TYPE` set to the new rule, `RULE_VALUE` holding the formula or pattern, and `RULE_ACTIVE=1`. Submit, approve, done - it's a config change, not a code release. The [MPE_VALIDATIONS table guide](https://docs.datacontroller.io/tables/mpe_validations/) has the full column reference.
2. **Serve.** When a user opens the editor, the [`editors/getdata`](https://code.datacontroller.io/getdata_8sas.html) service extracts the active rules for that table - it filters `MPE_VALIDATIONS` on library, table and `RULE_ACTIVE=1` - and returns them in the `dqrules` object of the response, alongside the table data, schema, and the schema-derived `NOTNULL` constraints.
3. **Apply.** The frontend wires each rule into the Handsontable grid: formula rules are computed live by [HyperFormula](https://hyperformula.handsontable.com/) (the calculation engine behind Handsontable itself), regex rules are evaluated in the browser with the JavaScript regex engine.
4. **Block or warn.** On submit, the standard [cell validation](https://docs.datacontroller.io/dcc-validations/) cycle runs: `HARD` rules block the submission if violated, `SOFT` rules warn but allow.
Here are the new rule types as they appear in the config - one row per rule, `RULE_VALUE` holding the formula or the pattern:
![](./mpe_validations_rules.png)
For regex there is also a config-time guard, and it's a neat piece of dogfooding: because the rule itself lives in a table, saving an edit to `MPE_VALIDATIONS` through Data Controller runs the [post-edit hook](https://code.datacontroller.io/mpe__validations__postedit_8sas.html), which passes every `HARDREGEX` / `SOFTREGEX` `RULE_VALUE` through `PRXPARSE` and rejects the edit if the pattern is invalid - listing the offending columns. A typo in a pattern is caught the moment you save the rule, not the first time a user hits it.
## Regex rules (HARDREGEX / SOFTREGEX, v7.12)
Regex rules validate cell values against a SAS (Perl-style) regular expression. You provide the pattern in `RULE_VALUE`; whether it blocks or warns depends on the rule type:
* **HARDREGEX** - the value **must** match the pattern. If it doesn't, the cell is highlighted red and submission is blocked.
* **SOFTREGEX** - a non-matching value is highlighted yellow as a warning. The user can still submit - it's a nudge, not a block.
### Setting one up
Say `SOME_CHAR` in the demo table must contain either "the" or "data" (case-insensitive). That's one insert:
```sas
insert into &lib..MPE_VALIDATIONS set
tx_from=0
,base_lib="&lib"
,base_ds="MPE_X_TEST"
,base_col="SOME_CHAR"
,rule_type='HARDREGEX'
,rule_value='/the|data/i'
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
```
That's a real row from the demo data, by the way - the shipped `MPE_X_TEST` table carries sample `HARDREGEX` and `SOFTREGEX` rules so you can try both without touching your own config.
In the editor it looks like this - an invalid email blocked red by `HARDREGEX`, an invalid postcode warned yellow by `SOFTREGEX`, and a value that passes:
![](./regex_demo.png)
Note the third column in that screenshot, `REGEX_BOTH_COL`. It carries _both_ a `HARDREGEX` and a `SOFTREGEX` rule - and shows neither warning. That's the precedence rule in action: only one regex is ever applied per column, and if both exist the `SOFTREGEX` is ignored entirely, so the column behaves exactly as if it were `HARDREGEX`-only.
### What to know before writing patterns
The full list of gotchas is in the [regex rules documentation](https://docs.datacontroller.io/dcc-validations/#regex-rules); the ones that matter most in practice:
* **Use the PRX delimiter form.** Patterns are authored exactly as [PRXPARSE](https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/lefunctionsref/p0s9ilagexmjl8n1u7e1t1jfnzlk.htm) accepts them: `/pattern/flags`, e.g. `/^\d+$/` for "integers only", `/the|data/i` for a case-insensitive contains. The config-time `PRXPARSE` check enforces this - a bare pattern without delimiters will be rejected when you save the rule. (The frontend tolerates bare patterns for backwards compatibility, but don't author new rules that way.)
* **Anchor your own patterns.** The pattern is used **as authored** - it is not auto-anchored. `/the|data/i` matches "the" _anywhere_ in the value. If you want the whole value to match, include `^` and `$` yourself.
* **Stick to the common subset.** The pattern is evaluated in the browser with the JavaScript regex engine, which shares SAS PRX's core syntax (character classes, quantifiers, groups, alternation, `^`/`$` anchors, `\d \w \s` and friends). Three unambiguous Perl-isms are translated automatically - a leading `(?i)` modifier, `\Q...\E` literal sequences, and `\A`/`\z` absolute anchors. But Perl-only constructs such as possessive quantifiers (`a++`) and atomic groups (`(?>...)`) pass the SAS-side check and then silently do nothing in the frontend - so just don't use them.
* **Blank is exempt.** Blank values skip pattern matching on any column type (use the `NOTNULL` rule if you also need populated values). On numeric columns the plain SAS missing (`.`) is also exempt - but special missings (`.A`-`.Z`, `._`) are not: they're deliberately-set values, so your pattern needs to accommodate them.
* **One regex per column.** As above - `HARDREGEX` wins when both are present, and the column-header info dropdown shows only the rule that is actually applied.
* **Length limit.** `RULE_VALUE` is 128 characters, which constrains very long patterns.
* **Deleted rows are exempt.** Cells in rows marked for deletion are not validated or warned (except primary key columns, which still are).
### Where regex beats a dropdown
For currency codes, country codes, account number formats, email shapes, or "must be an integer" - a regex is one row of config versus a 1000-value `HARDSELECT` dropdown. And unlike a dropdown, a regex catches _paste_ operations too, not just typed values.
## Formula rules (HARDFORMULA / SOFTFORMULA, v7.13)
Formula rules make a column compute itself from other columns in the same row - like a spreadsheet formula, but the formula lives in config and applies to every row. When a user opens the editor, the formula is evaluated live and the result is shown in each cell. Change an input, and every dependent cell recalculates on the spot.
### Writing a formula
Formulas use **column names, not cell references** - there is no need to know the grid layout. If you have `A_COL` and `B_COL`, a `FORMULA_HARD_COL` rule is simply:
```
=A_COL * B_COL
```
Each row calculates its own result: row 1's values, row 2's values, and so on. Under the hood each column name is translated to a row-relative cell reference (text inside quotes is left untouched) and handed to HyperFormula - so you get the full spreadsheet function library, `IF`, `SUM`, `ROUND`, `CONCAT` and friends, without writing any code.
The one syntax rule: **each column name must be surrounded by spaces**. `=MATCH( PRICE )` resolves the column reference; `=MATCH(PRICE)` does not - without the surrounding blanks the token isn't recognised as a column reference, and the formula errors rather than using the column's value. The spaces stop column names clashing with function names.
### HARD vs SOFT formulas
* **HARDFORMULA** - the column is read-only. The formula result is always shown and submitted; the user cannot change it. Think calculated amounts, or a `PROCESSED_BY` column.
* **SOFTFORMULA** - the cell shows the formula result, but the user can type a different value if the computed one is wrong. Their value is submitted instead. Useful for derived defaults where the business occasionally needs to override.
### Special values
Formulas can reference three runtime values, resolved when the formula is evaluated:
* `DC.ROW_STATUS` - the row's current state: `M` (Modified), `A` (Added), `D` (Deleted), or `U` (Unchanged). A newly-added row is `A` from the moment it is created - there is no transient state before that.
* `DC.USER_NAME` - the logged-in user id.
* `DC.ORIG_VALUE` - the original cell value before the current edit.
Our demo formula table puts them all to work. There's a row-status column (`=DC.ROW_STATUS`), a user column (`=DC.USER_NAME`), and a `CHANGE_SUMMARY_COL` whose rule reads like a proper audit sentence:
```
=IF( DC.ROW_STATUS ="U","unedited",
DC.USER_NAME &" changed from "& DC.ORIG_VALUE )
```
Here it is live in the editor. We edited `B_COL` on the second row from 10 to 25 - and the whole row reacted: `FORMULA_HARD_COL` recomputed to 50 (`A_COL * B_COL`), `FORMULA_SOFT_COL` recomputed to 27 (`A_COL + B_COL`), and the row status flipped from `U` to `M` - all instantly, all without touching a single line of SAS:
![](./formula_demo.png)
And here's the payoff of those `DC.*` references - the audit columns from the same table, same edit. `ROW_STATUS_COL` flipped to `M`, and the change summary resolved the `IF` formula above into a sentence - "sasdemo changed from orig-2":
![](./formula_audit_demo.png)
Because `DC.ROW_STATUS` is a live reference, it updates as the user works: edit a cell and the stamp flips to your user id; cancel the edit and it reverts.
### Editor behaviour worth knowing
* When you paste a formula into the grid, column names are automatically translated so the formula works in its new position.
* A cell that is overwritten by a formula is flagged (with the original value retained) so you can revert it.
* Formula-looking values pasted from Excel are treated as plain data (not evaluated), unless you explicitly choose "Apply as formula" - a deliberate safety measure so a spreadsheet's internal formulas don't leak into your data as live rules.
* When submitted, it's the formula's computed value that is sent to the backend, never the raw formula text (a primary key column always resolves its live formula immediately to the computed value so the submission keys are correct).
## How it works under the hood (briefly)
Two moving parts, and the boundary between them explains most of the gotchas above.
**Serving the rules.** [`getdata`](https://code.datacontroller.io/getdata_8sas.html) extracts the active `MPE_VALIDATIONS` rows for the target table and returns them in `dqrules`, along with the schema-derived `NOTNULL` constraints. Formulas and regex are frontend rules - they are evaluated in the browser, not in SAS - which is why they arrive as `dqrules` rather than as backend hook scripts.
**Evaluating them.** For formulas, the client translates each column name in `RULE_VALUE` to a row-relative cell reference and hands it to HyperFormula (wired into Handsontable's formulas plugin). For regex, the client parses the PRX `/pattern/flags` form, translates the three Perl-isms it can, and constructs a JavaScript `RegExp`. If a pattern still fails in the browser, the editor treats it as always-valid rather than breaking - a failed pattern never blocks a submission it shouldn't.
**Guarding the config.** Because `MPE_VALIDATIONS` is itself a Data Controller table, a [post-edit hook](https://code.datacontroller.io/mpe__validations__postedit_8sas.html) validates new rules: `PRXPARSE` checks every regex `RULE_VALUE`, and the edit is rejected with the offending columns listed. Invalid rules never reach users.
## Also in these releases
Beyond the headline features, the usual spread of hardening and polishing shipped along the way: row-header status cells are now colour-coded (with a `±` symbol for modified rows), CAS support landed for the `REPLACE` load type, Viya deploy diagnostics were improved, and a large tranche of dependency upgrades (Angular 20, Handsontable 18 pinned, sasjs core v5) keeps the audit trail clean. As ever, the full commit-by-commit detail is in the [release notes](https://git.datacontroller.io/dc/dc/releases).
## Upgrading
The frontend changes are included in the 7.13 release assets. The backend additions are **data-only, optional migrations**:
* The **v7.12 migration** adds `HARDREGEX` / `SOFTREGEX` to the `RULE_TYPE` dropdown in `MPE_VALIDATIONS` (and switches the `MPE_SECURITY.LIBREF` validation to a hook that lists all libraries).
* The **v7.13 migration** adds `HARDFORMULA` / `SOFTFORMULA` to the same dropdown.
Both scripts are in [`sas/sasjs/db/migrations/`](https://git.datacontroller.io/dc/dc/src/branch/main/sas/sasjs/db/migrations) in the source repo, and they're worth running even if you don't plan to use the rules immediately - they only add dropdown values to `MPE_SELECTBOX`.
## Try it yourself
The shipped demo data includes the regex rules on `MPE_X_TEST`, so you can see them working without configuring anything: open the demo library, edit `MPE_X_TEST`, and try entering a `SOME_SHORTNUM` between 1 and 5 (blocked red - `HARDREGEX`), a `PRIMARY_KEY_FIELD` with a decimal point (warned yellow - `SOFTREGEX`), or a `SOME_CHAR` without "the" or "data" in it (blocked - `HARDREGEX`).
For formulas, add a rule to one of your own tables - the `REVENUE = PRICE * VOLUME` example above is a two-minute configuration, and the `DC.*` special values make audit-style columns almost free. Full reference in the [validations docs](https://docs.datacontroller.io/dcc-validations/).
As ever - if you'd like to see additional validation types, [get in touch](https://datacontroller.io/pricing). The roadmap is customer-driven, and the validations list keeps growing.
<!-- Source LinkedIn post:
Every SAS team has a spreadsheet that "helps" load data into production.
You know the one. Someone built it years ago, nobody fully understands it, and it writes straight to a table it probably shouldn't.
What if that spreadsheet was wrong BEFORE anyone could hit send?
That's the idea behind Data Controller - a web app for SAS that lets business users change data safely. Every change is checked at the point of entry, goes for approval, and lands in a full audit trail. No new infrastructure - it runs on the SAS you already have.
The latest release adds two things that used to require a developer:
→ Formulas. Type a rule like = PRICE * VOLUME and the revenue column computes itself - spreadsheet-style, in the browser, live.
→ Pattern checks. "Must be a valid email." "Must be an integer." One row of config. Bad values are blocked or flagged before submission - not found in next month's report.
Both are configuration, not code. If you can fill in a table, you can set them up.
We wrote up how it works (with live screenshots of a bad email being caught red-handed):
Link in the comments 👇
#sas #dataquality #endusercomputing #excel #datagovernance
First comment: https://datacontroller.io/v7-13-formulas-and-regex/
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

@@ -0,0 +1,67 @@
---
title: 'The 5 Lines of Defence for Business Data Inputs'
description: How Data Controller for SAS® defends data quality for workflows deriving from business inputs - data model, permissions, validation checks, post edit hooks, and approvals.
date: '2026-08-27 09:00:00'
author: 'Allan Bowe'
authorLink: https://www.linkedin.com/in/allanbowe/
tags:
- Announcements
previewImg: './five-lines-of-defence.jpeg'
---
How does Data Controller for SAS® defend data quality for data workflows deriving from business inputs?
**⚔️ 1st Line of Defence - Data Model ⚔️**
The interface will only accept inputs conforming to this model (columns, types, lengths, indexes, constraints etc). The schema determines the behaviour - eg, a date format results in a date picker.
**⚔️ 2nd Line of Defence - Data Permissions ⚔️**
Changes are made using a SAS System Account (eg `sassrv`). This means you can safely DENY write-access to data for business users, preventing un-controlled data ingestion.
**⚔️ 3rd Line of Defence - Validation Checks ⚔️**
Additional validation checks (eg value ranges, specific patterns) can be configured to run at the point of data capture, prior to SAS upload. See the [validation documentation](https://docs.datacontroller.io/dcc-validations/) for details.
**⚔️ 4th Line of Defence - Post Edit Hook ⚔️**
Complex / customer specific validation can be deployed as SAS code, to run after every EDIT (prior to APPROVAL) using a [post edit hook](https://docs.datacontroller.io/dcc-tables/#post_edit_hook). A failure here results in immediate user feedback / change rejection.
**⚔️ 5th Line of Defence - Approval Step(s) ⚔️**
Changes are reviewed with [one or more approvals](https://docs.datacontroller.io/dcc-tables/) BEFORE being applied to the target database. A full audit trail is also maintained.
All functionality is ZERO-CODE, works on SAS Viya / EBI / SASjs Server, and applies to any database you have an ACCESS engine for. There is also a [community edition](https://datacontroller.io/pricing/) - free for unlimited users.
If you'd like to strengthen your own defences - [let's chat](https://datacontroller.io/contact/).
<!--
Source LinkedIn post:
How does Data Controller for SAS® defend #DataQuality for #DataWorkflows deriving from Business Inputs?
⚔️ 1st Line of Defence - Data Model ⚔️
The interface will only accept inputs conforming to this model (columns, types, lengths, indexes, constraints etc). The schema determines the behaviour - eg, a date format results in a date picker.
⚔️ 2nd Line of Defence - Data Permissions ⚔️
Changes are made using a SAS System Account (eg `sassrv`). This means you can safely DENY write-access to data for business users, preventing un-controlled #DataIngestion.
⚔️ 3rd Line of Defence - Validation checks ⚔️
Additional validation checks (eg value ranges, specific patterns) can be configured to run at the point of data capture, prior to #SAS upload. The docs for these are here: https://lnkd.in/djtaGPsr
⚔️ 4th Line of Defence - Post Edit Hook ⚔️
Complex / customer specific validation can be deployed as SAS code, to run after every EDIT (prior to APPROVAL). A failure here results in immediate user feedback / change rejection.
⚔️ 5th Line of Defence - Approval Step(s) ⚔️
Changes are reviewed with one or more approvals BEFORE being applied to the target database. A full audit trail is also maintained.
All functionality is ZERO-CODE, works on #sasViya / EBI / SASjs Server, and applies to any database you have an ACCESS engine for. There is also a Community edition - free for unlimited users.
#datagovernance #sasadmin #saspartners
-->
<!-- Image prompt:
A flat-design illustration in the Data Controller brand style (dark navy background, teal/green and orange accents), composed around a SINGLE CENTRAL focal point so it survives a square crop: a medieval-castle motif reimagined as a data fortress - five concentric defensive walls (rendered as clean glowing ring segments, each a slightly different teal/green shade, subtly numbered 1-5) surrounding a central database cylinder that glows safely at the core. An arrow or data-packet stream approaches from the top, passing checkpoints in each wall: a schema/grid icon (wall 1), a padlock (wall 2), a checklist with ticks (wall 3), a code angle-brackets icon (wall 4), and a stamp/approval tick (wall 5). One red/orange invalid packet is shown bouncing off an outer wall. All key elements within the central square of the frame; outer left and right thirds contain only background texture and glow, safe to crop. Minimal text, no logos. Professional but lightly playful, suitable for a B2B data product site. 16:9 landscape, 1200x627, suitable as a blog/feed cover image.
Generated with: local Routstr node (see blog-dev/skills routstr-image-generation), model gemini-3.1-flash-lite-image, output 1424x736 JPEG, ~31 sats.
-->
@@ -0,0 +1,72 @@
---
title: 'Struggling with Legacy Data Ingestion?'
description: Filename conventions, network drives, ancient formats and 5am batch failures - legacy spreadsheet ingestion hurts everyone. With Data Controller for SAS®, those issues disappear.
date: '2026-08-24 09:00:00'
author: 'Allan Bowe'
authorLink: https://www.linkedin.com/in/allanbowe/
tags:
- Announcements
previewImg: './legacy-data-ingestion.jpeg'
---
Do you struggle against legacy data ingestion processes?
Are your analysts saving spreadsheets with a particular filename, on a particular network drive, on a particular day, with a specific structure?
Perhaps your process necessitates an ancient format (.xls, Excel '95) or particular encoding.
Sucks, right?
It sucks also for the Platform Manager, who gets the email at 5am when the batch breaks due to something not-quite-right in said input file.
Or the Data Engineer, spending all day knocking out generic excel import routines. Or the sponsor, paying for a broken process, that should take seconds.
With Data Controller for SAS®, all these issues... disappear.
Thanks to our OEM licence of [SheetJS](https://sheetjs.com), analysts can self-serve data uploads via the browser - no (insecure) network drive needed, nor thick client install.
And the data really can be **anywhere** in the workbook - in any shape, spread across any number of disparate sheets, ranges and cells. A range of [import options](https://docs.datacontroller.io/excel/) handles everything from clean tabular sheets to sprawling financial reports.
Once automatic validations and DQ checks pass, and approval received, the target table is updated and any onward jobs are triggered. Needless to say, there is a full audit trail right back to the original excel file.
Not only that - you can load ANY database, SAS dataset, format, or CAS table. New data targets are configured by the admin - zero code, nothing to deploy or drag through to production.
There is also a [community edition](https://datacontroller.io/pricing/), free for unlimited users (restricted by rows).
If you'd like to excel by spending less time on Excel - [let's chat](https://datacontroller.io/contact/).
<!--
Source LinkedIn post:
Do you struggle against legacy data ingestion processes?
Are your analysts saving spreadsheets with a particular filename, on a particular network drive, on a particular day, with a specific structure?
Perhaps your process necessitates an ancient format (.xls, Excel '95) or particular encoding.
Sucks, right?
It sucks also for the Platform Manager, who gets the email at 5am when the batch breaks due to something not-quite-right in said input file.
Or the Data Engineer, spending all day knocking out generic excel import routines. Or the sponsor, paying for a broken process, that should take seconds.
With Data Controller for SAS®, all these issues... disappear.
Thanks to our OEM licence of SheetJS, analysts can self-serve data uploads via the browser - no (insecure) network drive needed, nor thick client install.
The data can be anywhere in the workbook - in any shape, across any number of disparate sheets, ranges and cells - thanks to a range of import options. Columns in any order. Additional columns (not in the target table) are simply ignored. Invalid columns cause immediate rejection, so the uploader can rectify.
Once automatic validations and DQ checks pass, and approval received, the target table is updated and any onward jobs are triggered. Needless to say, there is a full audit trail right back to the original excel file.
Not only that - you can load ANY database, #SAS dataset or CAS table. New data targets are configured by the admin - zero code, nothing to deploy or drag through to production.
There is also a community edition, free for unlimited users (restricted by rows).
If you'd like to excel by spending less time on Excel - let's chat.
#datamanagement #excel #datagovernance
-->
<!-- Image prompt:
A flat-design illustration in the Data Controller brand style (dark navy background, teal/green and orange accents), composed around a SINGLE CENTRAL focal point so it survives a square crop: in the centre, a large glowing browser window with a spreadsheet file being dropped into it and a green tick, feeding a short downward flow into a database cylinder directly beneath. Sinking into shadow at the very bottom of the frame, small and half-buried, the legacy relics being left behind: a floppy disk labelled .xls, a dusty network drive wrapped in chains, and an alarm clock showing 5am. Light radiates from the central browser window against the dark background. All key elements within the central square of the frame; outer left and right thirds contain only background texture and glow, safe to crop. Minimal text, no logos. Professional but lightly humorous, suitable for a B2B data product site. 16:9 landscape, 1200x627, suitable as a blog/feed cover image.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

@@ -0,0 +1,85 @@
---
title: 'Oops! Now You Can Roll Back Data Changes'
description: Despite all the checks in Data Controller for SAS®, sometimes the wrong updates get approved. You can now roll back to a previous version - with full audit history preserved.
date: '2026-08-20 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- Announcements
previewImg: './ctrl-z.jpeg'
---
# Oops! Now You Can Roll Back Data Changes
Despite the MANY checks and guarantees in Data Controller for SAS®, sometimes it can happen that the wrong updates are approved and applied.
Thankfully, it is now possible to **roll back** data changes to a previous state.
![Two trains colliding - the wrong data changes applied](rollback-trains-meme.jpeg)
## How it works in practice
Behind the scenes, rollback is not a silent undo. It is a **first-class approval workflow** just like any other edit. When you choose to restore a previous version, the backend - via the [`%mp_stripdiffs`](https://core.sasjs.io/mp__stripdiffs_8sas.html) macro - reads the `MPE_AUDIT` table (or a custom `AUDIT_LIBDS` configured for the table) and computes every difference between the current state and the version you want to go back to.
It handles all three change types:
- **Deleted rows** are re-inserted with their original values.
- **Modified rows** are reverted to their previous values.
- **Added rows** are marked for deletion with `_____DELETE__THIS__RECORD_____="Yes"`.
The computed differences are written to a new staging package in the approvals directory, complete with a CSV and a `macvars.sas` snapshot of the session context. A new `LOAD_REF` is generated, and the package is submitted via the standard `%mpe_loader` service - so it goes through the same edit-stage-approve workflow as any manual change.
This means the rollback itself is **reviewable and approvable**. Nothing is applied silently, and the full audit trail is maintained: the reversion appears as a new load reference in `MPE_SUBMIT`, `MPE_REVIEW`, `MPE_DATALOADS`, and `MPE_AUDIT`, just like any other submission.
## Security and access
Not everyone can roll back everything. The `%mpe_checkrestore` macro enforces a strict access check before the restore service will run:
- The load must actually exist and have been loaded (no rollbacks of unapproved submissions).
- The table must be configured with an audit table.
- The user must have `EDIT` access to the target table.
- If the user is not an admin, Row Level Security or Column Level Security rules on the table will block the restore.
If access is denied, the service aborts with a clear reason - no opaque errors.
## What this means for compliance
Because the rollback creates a new, approved changeset rather than silently rewinding history, auditors can see exactly what was reverted, when, and by whom. The `MPE_AUDIT` table retains the record of every intermediate state, so nothing is ever truly lost. For tables where data integrity is critical - regulatory reporting, actuarial assumptions, steering parameters - this is the difference between "we have no idea what happened" and "here is the complete chain of custody."
This feature works for all temporal-aware load types (`UPDATE`, `TXTEMPORAL`, and `BITEMPORAL`) and respects SCD2 validity windows. And the whole process is built on the same open-source macro library that powers the rest of Data Controller - so you can inspect, test, and audit the code itself.
Full documentation is here: https://docs.datacontroller.io/restore/
<!--
Source LinkedIn post:
"Can we put it back the way it was?" should never be a hard question to answer.
Yet for many teams managing reference data, mappings, and regulatory adjustments in SAS®, a wrong approval means exactly that — a forensic exercise, an uncomfortable audit conversation, and a quiet hope that nobody upstream consumed the bad data.
Data Controller now lets you ROLL BACK a table to any previous state.
Not by silently rewinding history - the one thing your auditor definitely does not want. Instead, the reversion is packaged as a brand NEW change that goes through the same review and approval as any other edit:
- The reversion diff is calculated and staged
- An approver reviews it before anything is applied
- The rollback itself lands in the audit trail - who, what, when, why
Nothing disappears. The original mistake, its correction, and every state in between all remain fully traceable.
Access rules apply exactly as they do for edits - the same group permissions, the same restrictions. A rollback cannot be used as a shortcut around your controls.
For regulated reporting data - the kind where "oops" is a reportable event, not a shrug - this closes the loop between catching an error and evidencing its correction.
The underlying process is open source.
Link in the comments below 👇
#sas #sasviya #datagovernance #datamanagement
-->
<!-- Image prompt:
A dramatic flat-design meme illustration in the Data Controller brand style: two high-speed trains colliding head-on at the center of the frame, with data rows and spreadsheet cells flying out of the impact. A group of onlookers in business-casual attire stand in the foreground, heads bowed, looking on in shared sadness. Dark navy background, teal/green and muted orange accents. The collision represents conflicting data changes; the sad onlookers are the data stewards. Minimal text, no labels. Slightly stylised, not gory - suitable for a B2B data product audience. 16:9 landscape, suitable as a blog/feed cover image.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

+5
View File
@@ -21,6 +21,11 @@ const Footer = () => (
on <StyledAnchor href="https://sasapps.io">SAS Web Apps</StyledAnchor> on <StyledAnchor href="https://sasapps.io">SAS Web Apps</StyledAnchor>
. .
</StyledDesc> </StyledDesc>
<StyledDesc>
<StyledAnchor href="https://sasapps.io/cyber-essentials-certified/">
Cyber Essentials Certified
</StyledAnchor>
</StyledDesc>
</div> </div>
<div className="col-md-3"> <div className="col-md-3">
<StyledHeading>Source Code</StyledHeading> <StyledHeading>Source Code</StyledHeading>
+11 -1
View File
@@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import { FaShieldAlt } from 'react-icons/fa'
import { Hero, HeroHeading, HeroDesc } from './style' import { Hero, HeroHeading, HeroDesc, HeroBadge } from './style'
import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents' import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents'
import { Container } from '../shared' import { Container } from '../shared'
@@ -19,9 +20,18 @@ const HeroSection: React.FC<HeroSectionProps> = ({
<HeroHeading>{heading}</HeroHeading> <HeroHeading>{heading}</HeroHeading>
<HeroDesc>{desc}</HeroDesc> <HeroDesc>{desc}</HeroDesc>
{location.pathname === pathPrefix + '/' && ( {location.pathname === pathPrefix + '/' && (
<>
<HeroBadge
href="https://sasapps.io/cyber-essentials-certified/"
target="_blank"
rel="noopener"
>
<FaShieldAlt /> Cyber Essentials Certified
</HeroBadge>
<Link to="/contact/"> <Link to="/contact/">
<OutlineButton>Try Data Controller</OutlineButton> <OutlineButton>Try Data Controller</OutlineButton>
</Link> </Link>
</>
)} )}
</Container> </Container>
<BottomSectionArrow /> <BottomSectionArrow />
+17
View File
@@ -17,3 +17,20 @@ export const HeroHeading = styled.h1`
export const HeroDesc = styled.p` export const HeroDesc = styled.p`
opacity: 0.8; opacity: 0.8;
` `
export const HeroBadge = styled.a`
display: block;
width: fit-content;
margin: 0.75rem 0 0;
padding: 0.375rem 1rem;
font-size: 0.75rem;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 0.25rem;
color: white;
text-decoration: none;
&:hover {
color: white;
border-color: white;
opacity: 0.9;
}
`