Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59f169e32d | ||
|
|
1a5338e876 | ||
|
|
bbb163c25c | ||
|
|
2c7a3b8465 | ||
|
|
d37538264a | ||
|
|
cd5eeafa08 | ||
|
|
4ef2b5333a | ||
|
|
3d2ec5869b | ||
|
|
6cabe6796a | ||
|
|
ce479a2c51 | ||
|
|
bc27894b38 | ||
|
|
f38b5d795c | ||
|
|
67cf263ec5 | ||
|
|
db969f51b4 | ||
|
|
8d4a82f19a | ||
|
|
cb072bf60a | ||
|
|
365b178496 | ||
|
|
b75e0bb38e | ||
|
|
b61ec07c87 | ||
|
|
6469ad00a3 | ||
|
|
ed20e0b982 | ||
|
|
42cf023efb | ||
|
|
ae3f5128c2 | ||
|
|
bc44d4cf73 | ||
|
|
838bc7a96f | ||
|
|
53c0de0960 | ||
|
|
549ecc36aa | ||
|
|
463de81ffa | ||
|
|
79d2e887d3 | ||
|
|
1119645a00 | ||
|
|
a1513b871e |
No files matched your search
@@ -4,6 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# One deploy at a time: `surfer deploy` swaps the site atomically, so two runs
|
||||
# racing each other is not corruption, but it is a wasted build and the last
|
||||
# swap wins.
|
||||
concurrency:
|
||||
group: surfer-datacontroller-io
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
Build-and-publish:
|
||||
@@ -21,5 +29,23 @@ jobs:
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
# Deploy tooling pinned to an exact version: the workflow publishes
|
||||
# whatever is committed, and an unpinned `latest` would let a breaking or
|
||||
# compromised upstream release ship straight to the live site.
|
||||
#
|
||||
# `deploy` (v7) replaces the site atomically - it extracts to a staging
|
||||
# folder and swaps it in, so a failed upload leaves the current site
|
||||
# serving. The v6 `put` this replaces was token authenticated and is
|
||||
# gone; v7 takes the Cloudron username plus an app password for this app.
|
||||
- name: Publish
|
||||
run: npx surfer put --token ${{ secrets.SURFER_TOKEN }} --server datacontroller.io public/* /
|
||||
env:
|
||||
SURFER_PASSWORD: ${{ secrets.SURFER_PASSWORD }}
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npx --yes @cloudron/surfer@7.2.0 deploy public \
|
||||
--server datacontroller.io \
|
||||
--username allan \
|
||||
--password "$SURFER_PASSWORD" \
|
||||
--message "Build from ${REF_NAME} @ ${COMMIT_SHA:0:8}"
|
||||
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: 'Derived Variables: computed columns in the EDIT screen'
|
||||
description: Data Controller can now compute columns that do not exist in the table - and, crucially, filter on them. Configured per table in MPE_DERIVED, computed on every read, never stored.
|
||||
date: '2026-10-21 12:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './derived-grid.png'
|
||||
tags:
|
||||
- Data Controller
|
||||
- Features
|
||||
---
|
||||
|
||||
# Derived variables: computed columns you can filter on
|
||||
|
||||
Every reporting team has a version of this request, and it always sounds like a five minute job: a column in the EDIT screen that is not in the table. A region worked out from a state code. A line value that is quantity times price. Not stored, not loaded, just *there* - and, as the requester usually adds, filterable.
|
||||
|
||||
The "not stored" part is easy. The "filterable" part is where the design lives, and it is worth writing down, because it explains every other decision in the feature.
|
||||
|
||||
## Why a computed column is not automatically filterable
|
||||
|
||||
You would write it in a DATA step:
|
||||
|
||||
```sas
|
||||
data out;
|
||||
set mylib.mytable;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
where REGION = 'West';
|
||||
run;
|
||||
```
|
||||
|
||||
That fails. A `WHERE` statement applies to the data set named on the `SET` statement - the *input* - and on that input `REGION` does not exist. SAS cannot see a variable that the same step creates later. You can put the assignment and the filter in separate steps, but then the filter runs after the step has read every row, which is precisely the work the filter exists to avoid - and for a large table that is the difference between a fast screen and a coffee break.
|
||||
|
||||
So the calculation has to happen somewhere the filter can see it. In Data Controller it happens in a **DATA step view**:
|
||||
|
||||
```sas
|
||||
data work.vw_derived / view=work.vw_derived;
|
||||
set mylib.mytable;
|
||||
attrib REGION length=$8 label='REGION=ifc(...)' format=$8.;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
run;
|
||||
|
||||
data out;
|
||||
set work.vw_derived;
|
||||
where %inc filtref;;
|
||||
run;
|
||||
```
|
||||
|
||||
The view is not executed when it is defined. When the second step reads it, the assignment runs as part of the view's step, so by the time the `WHERE` is evaluated `REGION` is a column of the input. The filter is unchanged, the row-level security clause is unchanged, and nothing extra is read.
|
||||
|
||||
## What it looks like
|
||||
|
||||
The demo table below has four rows. `LINE_VALUE` is `QTY * PRICE`, `REGION` is derived from `STATE`, and `REGION_LABEL` is built on `REGION`. None of the three is a column of the table, and all three are read-only in the grid - and marked so, with a tinted header carrying an `fx` badge, italic tinted cells, and an edge where the computed block begins, so that "computed" never has to be confused with "locked by a rule":
|
||||
|
||||

|
||||
|
||||
Because they are on the input to the filter, they filter like any other column. Here is `REGION = 'West'`, with the filter bar naming the derived variable:
|
||||
|
||||

|
||||
|
||||
and a numeric one, `LINE_VALUE > 50`:
|
||||
|
||||

|
||||
|
||||
The value list in the filter dialog is built the same way, from the table under the rest of the filter - so a derived column gets a dropdown of its distinct values, not a blank box.
|
||||
|
||||
## Configuration
|
||||
|
||||
`MPE_DERIVED` is itself a Data Controller table, so it is maintained through the EDIT screen with the usual submit / review / approve workflow. One row per variable - these are the three rows behind the screenshots above:
|
||||
|
||||

|
||||
|
||||
|
||||
```
|
||||
VAR_NM REGION
|
||||
VAR_DEPS STATE
|
||||
VAR_TYPE C
|
||||
VAR_LEN 8
|
||||
VAR_FMT (blank)
|
||||
VAR_LABEL (blank)
|
||||
VAR_LOGIC ifc(STATE in ('CA','OR','WA'),'West','East')
|
||||
ACTIVE 1
|
||||
```
|
||||
|
||||
and the chained one:
|
||||
|
||||
```
|
||||
VAR_NM REGION_LABEL
|
||||
VAR_DEPS REGION STATE
|
||||
VAR_TYPE C
|
||||
VAR_LEN 14
|
||||
VAR_FMT (blank)
|
||||
VAR_LABEL Region and state
|
||||
VAR_LOGIC cats(REGION,'-',STATE)
|
||||
ACTIVE 1
|
||||
```
|
||||
|
||||
Which produces, at read time:
|
||||
|
||||
```sas
|
||||
attrib REGION length=$8 label='REGION=ifc(STATE in (''CA'',''OR'',''WA''),''West'',''East'')' format=$8.;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
attrib REGION_LABEL length=$14 label='Region and state' format=$14.;
|
||||
REGION_LABEL = cats(REGION,'-',STATE);
|
||||
```
|
||||
|
||||
Type and length are declared rather than guessed, so a value is truncated exactly as specified. The format is optional; when it is left blank a character variable gets `$<length>.` and a numeric one `<length>.`, and a date, time or datetime format makes the column render - and filter - as a date, the same way a stored column does. The label is `VAR_LABEL` when you supply one, otherwise the name, an equals sign and the expression, so a user who switches labels on can see where the value came from.
|
||||
|
||||
## Dependencies, without a sequence number
|
||||
|
||||
`REGION_LABEL` needs `REGION` to exist first. Rather than maintain a sort order, each row declares its inputs in `VAR_DEPS`, and the statements are emitted in dependency order. Adding a variable in the middle of a chain means editing one row's `VAR_DEPS`, not renumbering the table.
|
||||
|
||||
`VAR_DEPS` lists *every* input - physical columns and derived variables alike - and the declaration is checked against the expression in both directions: a derived variable the expression references must be declared, and a declared name the expression does not reference is an error. A declared name must be a column of the table or a derived variable of the same table, and a cycle is rejected with the path that forms it:
|
||||
|
||||
```
|
||||
VAR_DEPS contains a cycle: A -> B -> C -> A
|
||||
```
|
||||
|
||||
## What it will not do
|
||||
|
||||
**Expressions only.** `VAR_LOGIC` must be a single SAS expression. Semicolons, macro triggers (`%`, `&`) and comments are rejected, so one row can never be more than an expression - which is the point: a config table is not a place to write programs. Conditional logic is available through functions such as `ifc()`, `choosec()`, `cats()`, `catx()`, `put()`, `input()` and `intnx()`.
|
||||
|
||||
**No shadowing, no self-reference.** A derived variable may not reuse the name of a column in the target table. Nor may it reference itself: `SUMMER = SUMMER + 1` implies a retained value, which depends on the order rows happen to arrive in.
|
||||
|
||||
**Never stored.** Derived values are computed on every read. Nothing derived is submitted, reviewed or written back - the approval package contains physical columns only, and a crafted submission that includes a derived column is discarded before the security check runs.
|
||||
|
||||
**EDIT screen only, for now.** The VIEW screen and the download services do not show derived columns. A saved filter that references one still applies there and is evaluated the same way.
|
||||
|
||||
**Not for row level security.** A rule in `MPE_ROW_LEVEL_SECURITY` cannot reference a derived column: the rule is evaluated against the submitted data, where the column does not exist. Saving a rule that names one is rejected with an explanation rather than left to fail at submit time.
|
||||
|
||||
## One warning
|
||||
|
||||
An expression can reference a column that is hidden from a user by [Column Level Security](https://docs.datacontroller.io/column-level-security/) - and the derived column will show the result. Configure derived variables with the same care as any other data exposure.
|
||||
|
||||
## Availability
|
||||
|
||||
Derived variables are available on SAS 9, Viya and SASjs Server. The configuration table, the settings and the post-edit validation are created by the usual upgrade - there is nothing to install beyond the release itself.
|
||||
|
||||
The full reference, including every column of `MPE_DERIVED` and the validation rules applied at submit time, is in the [docs](https://docs.datacontroller.io/derived-variables/).
|
||||
|
||||
If a table of yours has been waiting for "just one more column", we would love to [hear from you](/contact).
|
||||
|
Before Width: | Height: | Size: 50 KiB |
@@ -1,128 +0,0 @@
|
||||
---
|
||||
title: 'One Table, Two Rule Sets'
|
||||
description: 'A single physical table can carry only one set of Data Controller validation rules. Two librefs over the same data - or a pair of hook scripts - give you as many rule sets as you need.'
|
||||
date: '2026-09-24 15:30:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Data Quality
|
||||
- Configuration
|
||||
previewImg: './rule-set-1.png'
|
||||
---
|
||||
|
||||
# One Table, Two Rule Sets
|
||||
|
||||
Most Data Controller sites settle into an obvious mapping: one table, one edit screen, one set of validation rules. But that is not always what the business wants. A table of orders might be edited from a finance report that only tolerates small adjustments, and from an operations report where much larger ones are routine. Same table, same columns, same approvers - different rules.
|
||||
|
||||
Data Controller's validation rules are configured per table, so this takes a little thought. There are two ways to do it: the one we recommend, and the one to reach for when the first is not available.
|
||||
|
||||
## Why one table is one rule set
|
||||
|
||||
Two configuration tables decide this.
|
||||
|
||||
`MPE_TABLES` is the list of editable tables, and its primary key is `(tx_from, libref, dsn)`. One physical table is one editable table.
|
||||
|
||||
`MPE_VALIDATIONS` holds the rules, and its primary key is `(tx_from, base_lib, base_ds, base_col, rule_type)`. Rules hang off a physical `libref.dataset`. There is no per-menu or per-report scoping anywhere in the schema, and the editor is handed exactly the rules whose `base_lib` and `base_ds` match the table being opened.
|
||||
|
||||
So two rule sets on one table need two distinct `libref.dataset` identities. The question is how to get them without copying the data.
|
||||
|
||||
## Option 1 (recommended): two librefs over the same data
|
||||
|
||||
A libref is just a name pointing at a location. Nothing stops you assigning two of them to the same place, and Data Controller will treat the two as separate tables:
|
||||
|
||||
```sas
|
||||
libname ORDERS_EU '/data/orders';
|
||||
libname ORDERS_US '/data/orders';
|
||||
```
|
||||
|
||||
`ORDERS_EU.ORDERS` and `ORDERS_US.ORDERS` are now the same physical file, but they are different rows in `MPE_TABLES` and can therefore carry different rows in `MPE_VALIDATIONS`. Register both, give each its own rules, and point each report at its own editor URL - `#/editor/ORDERS_EU.ORDERS` and `#/editor/ORDERS_US.ORDERS`.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Everything else works exactly as it always did. Filtering, search, the row cap, the approval diff and the audit trail all operate on the table as normal, because as far as Data Controller is concerned these are ordinary tables. The only difference is that the two names resolve to the same file, so an approval in either report updates the same data - and that one difference has a consequence for concurrency, which is covered below.
|
||||
|
||||
There is no copy to keep in sync, no hook to write and nothing to maintain. That is why it is the option we recommend.
|
||||
|
||||
### Things worth knowing
|
||||
|
||||
- Where the librefs are defined depends on your platform. On Viya, in the compute context's `autoexec.sas` (or `[DC Drive Path]/services/settings.sas`); on SAS 9, as metadata libraries or in the Data Controller Settings stored process; on SASjs Server, in `services/public/settings.sas`. The one requirement is that each library has a unique libref.
|
||||
- `mp_lockanytable` keys on `libref.dataset`, so the two menus do not serialise against each other. That has consequences beyond a collision - see [the concurrency caveat](#the-concurrency-caveat) below.
|
||||
- The audit trail and approval queue record which libref a change came through, so `ORDERS_EU.ORDERS` and `ORDERS_US.ORDERS` stay distinguishable in history. For most people that is a feature - you can see which report a change originated from.
|
||||
|
||||
### The concurrency caveat
|
||||
|
||||
Data Controller serialises its writes with `mp_lockanytable`, and the control table it uses, `MPE_LOCKANYTABLE`, has a primary key of `(lock_lib, lock_ds)`. That is the *registration*, not the physical file. At approve time, `postdata` takes the lock on the base table named in the submit record, and the same service runs its "has this table been updated since the diff screen was shown" check against `MPE_DATALOADS`, keyed on that same `libref` and `dsn`.
|
||||
|
||||
Two librefs over one location are two different `(libref, dsn)` pairs, so:
|
||||
|
||||
- two approvals arriving through different reports take two different locks, and neither one blocks the other;
|
||||
- a load through one report writes its `MPE_DATALOADS` entry against its own name, so the other report's staleness check does not see it either.
|
||||
|
||||
So two people editing the same rows through different reports can both be approved, and the second write wins, silently. The lock is advisory to begin with - `mp_lockanytable` is, in its own words, "only useful if every update uses the macro" - so this is not a new class of risk, but registering one file twice is a new way to fall into it.
|
||||
|
||||
The hook route does not have this problem. Both mirrors route their submit to the same base table, so every approval locks, loads and logs against one identity.
|
||||
|
||||
If you do use two librefs, and concurrency matters to you, the cleanest fix is to keep the two registrations for the edit screen and give each one a `POST_EDIT_HOOK` that re-points the changeset at a single canonical registration - then every approval is keyed on the same table regardless of which report raised it. Register that canonical name as well. The alternative, an explicit shared lock taken in `PRE_APPROVE_HOOK` and released in `POST_APPROVE_HOOK`, works too, but a failed run leaves the sentinel locked and `MPE_LOCKANYTABLE` is then a table you have to unpick by hand.
|
||||
|
||||
## Option 2: an empty mirror and a pair of hook scripts
|
||||
|
||||
Sometimes two librefs over one location are not available: a database library where the platform will not let you define the same object twice, or a site where adding a library definition is a change nobody wants to make. Then you can reach the same result with a mirror table and two hook scripts.
|
||||
|
||||
The idea is that the thing Data Controller edits is not the real table at all, but an empty table of the same shape, with hooks moving data in and out of it:
|
||||
|
||||
- a `PRE_EDIT_HOOK` fills the editor with the live rows of the real table, so the mirror never has to hold a copy
|
||||
- a `POST_EDIT_HOOK` re-points the submitted changeset at the real table, so the approval is raised against the real table and the load writes there
|
||||
|
||||
The mirror exists purely to carry the rule set, and never stores any data.
|
||||
|
||||
### The pre-edit hook
|
||||
|
||||
`PRE_EDIT_HOOK` runs inside the `getdata` service, after the user's filter has been applied and the rows sorted, with the data in `work.OUT`. It may replace that dataset, which is all this needs:
|
||||
|
||||
```sas
|
||||
data work.out;
|
||||
set ORDERS.ORDERS;
|
||||
run;
|
||||
```
|
||||
|
||||
The registered table is `ORDERS.MIRROR`, which is empty - so without the hook the editor would show nothing at all. With it, the grid shows the live rows:
|
||||
|
||||

|
||||
|
||||
Note the title bar: the mirror really is empty. Everything on screen came from the hook, and the grid is validated against the mirror's own rules rather than the real table's.
|
||||
|
||||
### The post-edit hook
|
||||
|
||||
This is the part that surprises people. `POST_EDIT_HOOK` runs inside the `mpe_loader` macro at submit time, on the staged rows, before the submit record is written. It cannot choose the target table directly - but at that point `LIBREF` and `DS` are still ordinary macro variables, and the submit record is built from them. Reassigning them re-points the whole changeset:
|
||||
|
||||
```sas
|
||||
data _null_;
|
||||
call symputx('libref','ORDERS');
|
||||
call symputx('ds','ORDERS');
|
||||
run;
|
||||
```
|
||||
|
||||
From that moment the changeset is an approval against `ORDERS.ORDERS`. The approver sees a diff against the real table, the load writes to the real table, and the mirror is never touched.
|
||||
|
||||

|
||||
|
||||
### The detail that will bite you
|
||||
|
||||
Use `call symputx`, not `%let`. `LIBREF` and `DS` are not declared `%local` in `mpe_loader`, and the hook is included into that scope - so `call symputx` finds the existing variable and updates it, while a `%let` creates a new variable in the hook's own scope and is silently discarded. The hook runs, the log looks clean, and the changeset goes to the mirror anyway.
|
||||
|
||||
### Other things to watch
|
||||
|
||||
- Concurrency behaves correctly here. Both mirrors route their submit to the same base table, so every approval locks, loads and logs against one identity - unlike the two-libref route above.
|
||||
- The filter has already been applied to the empty mirror by the time the pre-edit hook runs, so a hook that reads the real table ignores the user's filter unless it re-applies it (`where %inc filtref`). On a small table you will not notice; on a large one the `DC_MAXOBS_WEBEDIT` cap will stop the edit screen with "Table is too big".
|
||||
- The hook's output must have the same columns the editor expects - the real table, minus any transaction or processing columns that Data Controller drops on load.
|
||||
- The real table must itself be registered in `MPE_TABLES`. The approval screen resolves the table's audit settings from that row, so a changeset routed to a table with no registration cannot be reviewed - the submit is refused up front, naming the table.
|
||||
- The mirror's `MPE_TABLES` row is read for the edit screen and the real table's for the load, so keep their `buskey`, `loadtype` and temporal column settings identical.
|
||||
- At approval time the access checks run against the real table, so editors need `EDIT` on the mirror while approvers need `EDIT` and `APPROVE` on the real table.
|
||||
|
||||
## Which should you use?
|
||||
|
||||
If you can define two librefs over the same data, do that. It is configuration only, it leaves every other behaviour of the editor untouched, and there is nothing to maintain.
|
||||
|
||||
Reach for the hook scripts when the platform will not let you duplicate the library definition, or when you specifically want the rule set to be a property of the application rather than of the data.
|
||||
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: 'The column that is not in the table'
|
||||
description: 'Derived variables give the EDIT screen columns that do not exist in the table - computed on every read, never stored, and filterable like any other column. The full story is in the article.'
|
||||
date: '2026-10-27 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
- Features
|
||||
previewImg: './derived-fx.png'
|
||||
---
|
||||
|
||||
# The column that is not in the table
|
||||
|
||||
Every reporting team has a version of this request, and it always sounds like a five minute job: "can we have a region column? It is just the state, mapped. And a line value - quantity times price. We do not need it stored, we just need it on the screen."
|
||||
|
||||
It is never a five minute job - so Data Controller now does the obvious thing instead. You write the expression, and the EDIT screen shows the column: computed on every read, never stored, marked `fx` and read-only, so nobody has to wonder whether it is locked or simply has nothing to edit.
|
||||
|
||||
The part that took the thinking is that they filter like any other column. That needed a DATA step view rather than a data step, and the reasoning is worth reading: [Derived variables: computed columns you can filter on](/derived-variables/).
|
||||
|
||||
Reference: [Derived Variables](https://docs.datacontroller.io/derived-variables/).
|
||||
|
||||
If a table of yours has been waiting for "just one more column", we would love to [hear from you](/contact).
|
||||
|
||||
<!--
|
||||
Feed teaser for the article at /derived-variables/. The article carries the
|
||||
engineering - why a computed column is not automatically filterable, the DATA
|
||||
step view, the configuration table, the limits - so this post links to it
|
||||
rather than repeating it.
|
||||
|
||||
Image prompt (cover): a clean, modern illustration on a white background: a
|
||||
spreadsheet grid rendered in soft slate and light blue, with the leftmost four
|
||||
columns showing plain data and three further columns floating slightly to the
|
||||
right, detached from the table, tinted light blue and italicised, each marked
|
||||
with a small "fx" badge. A thin left border separates the floating block from
|
||||
the stored columns. No text other than the fx badges. Flat vector style, brand
|
||||
palette: slate #314351, light blue #d3e5f2, accent green #79a843 used sparingly
|
||||
as a single dot.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: 'Data Controller for SAS® - a Customer Story'
|
||||
description: Data Controller for SAS® lets business users self serve their data updates - restricted access to the data values, with the data model protected.
|
||||
date: '2026-10-11 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './customer-story.jpeg'
|
||||
---
|
||||
|
||||
# Data Controller for SAS® - a Customer Story
|
||||
|
||||
Data Controller for SAS® empowers business users to self serve their data updates, by providing restricted access to the data *values* whilst protecting the data *model*. This short customer story shows what that looks like for Data and Analytics teams in regulated industries.
|
||||
|
||||
The distinction is the whole point. An editor can add, correct and delete rows in the tables they are responsible for, but they cannot change the structure underneath - no new columns, no altered formats, no dropped keys. The shape of the table is fixed, and only its contents move. Every change is staged, validated at the point of entry, reviewed as a diff and approved by a data owner before it is loaded, with a full audit trail of who changed what, when and why.
|
||||
|
||||
See it in action:
|
||||
|
||||
<iframe title="Data Controller for SAS - a Customer Story" width="560" height="315" src="https://vid.4gl.io/videos/embed/kpSGoNE26FSif8X9A6pUfe" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||
|
||||
Would you like to see how this would work against your own tables? [Get in touch](/contact) to arrange a walkthrough.
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Business users need to keep their data current. They do not need to change the tables underneath.
|
||||
|
||||
Most tools blur the two. Hand someone a spreadsheet and they reshape the file. Grant write access to a production table and they can reshape the table. Neither is what you want.
|
||||
|
||||
Data Controller for SAS® draws the line at the data VALUES. Business users self serve their updates - adding, correcting and deleting rows in the tables they own - with restricted access to what is inside the table, whilst the data MODEL stays protected. No new columns, no altered formats, no dropped keys.
|
||||
|
||||
→ Every change is staged and validated at the point of entry
|
||||
→ Reviewed as a diff and approved by a data owner before it loads
|
||||
→ With a full audit trail of who changed what, when and why
|
||||
|
||||
A short customer story, for Data and Analytics teams in regulated industries:
|
||||
|
||||
✅ Watch the story, then book a Discovery Call - link in the comments.
|
||||
|
||||
#sas #sasviya #datagovernance #datacapture #dataquality
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
|
||||
Generate a 16:9 landscape illustration for a B2B data-governance blog cover, 1200x627.
|
||||
|
||||
Style: flat vector illustration, dark slate background (#314351) with a faint evenly spaced dot grid, brand green accent (#90c445), lighter slate panels (#2e4252), soft diffused shadows, subtle depth.
|
||||
|
||||
Scene: a single data table drawn as two separated layers. The top layer is the table's structure - a header band of column blocks in muted slate, held behind a closed green padlock on the left. The bottom layer is the table's values - four rows of rounded cells in lighter slate, with two cells filled solid brand green and a small green pencil resting beside the lower right of the grid, as if those values are the ones being edited. A thin vertical gap separates the two layers, so the padlock reads as protecting the structure above while the open cells below are the editable values.
|
||||
|
||||
Minimal text, no letters, no numbers, no logos, no watermark. Keep the subject inside the central square so a 1:1 crop is safe; the outer left and right thirds may be croppable background only.
|
||||
|
||||
Output: save as ./customer-story.jpeg at 1200x627 (matching the other feed covers, and doubling as the LinkedIn share card). Do not also embed the image in the markdown body.
|
||||
-->
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: 'Support Diagnostics: browser_info and browser_url_vars'
|
||||
description: Data Controller sends two input tables with the startup service and with every service that runs customer code - the browser context and the page URL parameters. A support ticket can be answered from the job log, and a hook can read them.
|
||||
date: '2026-09-27 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './support-diagnostics.jpeg'
|
||||
---
|
||||
|
||||
# Support Diagnostics: browser_info and browser_url_vars
|
||||
|
||||
A support ticket usually starts with a round of questions. Which browser? Which timezone? Which build? What was in the URL? Data Controller now answers them itself. The frontend sends two input tables - `browser_info` and `browser_url_vars` - with the startup service and with every service that runs customer-provided code, so the context of a session is in the job log before anyone asks for it, and a hook script can read it.
|
||||
|
||||
This ships with the next Data Controller release.
|
||||
|
||||
## The two tables
|
||||
|
||||
Both are ordinary input tables on the request: the adapter turns each array in the payload into a table in the service's WORK library, so they arrive as `work.browser_info` and `work.browser_url_vars`.
|
||||
|
||||
### browser_info
|
||||
|
||||
A single row, describing the client that made the request:
|
||||
|
||||
| Column | What it holds |
|
||||
|---|---|
|
||||
| `url` | The URL of the Data Controller page itself (the iframe), not the document embedding it - so an embedded editor is distinguishable from a standalone one. |
|
||||
| `referrer` | The embedding document, from `document.referrer`. For an embedded report this is the report URL. |
|
||||
| `timezone` | The browser's IANA timezone, e.g. `Europe/Berlin`. |
|
||||
| `tz_offset` | The browser's UTC offset in minutes, as `Date.getTimezoneOffset()` returns it - positive west of UTC, so `Europe/Berlin` in summer is `-120`. |
|
||||
| `locale` | The browser locale, e.g. `en-GB`. |
|
||||
| `dc_version` | The Data Controller build, as `appinfo()` reports it. |
|
||||
| `adapter_version` | The `@sasjs/adapter` version the client was built with. |
|
||||
| `browser` | The browser family, parsed from the user agent. |
|
||||
| `browser_version` | The browser version. |
|
||||
| `platform` | The operating system. |
|
||||
| `user_agent` | The raw `navigator.userAgent` string. |
|
||||
|
||||
### browser_url_vars
|
||||
|
||||
One row per URL parameter:
|
||||
|
||||
| Column | What it holds |
|
||||
|---|---|
|
||||
| `name` | The parameter name. |
|
||||
| `value` | The parameter value. |
|
||||
|
||||
Parameters are read from both the search string and the hash query string, because Angular routes carry them after the `#`. Where the same name appears in both, the search string wins. The table is sent only when the page URL has at least one parameter.
|
||||
|
||||
## Which services receive them
|
||||
|
||||
| Service | When |
|
||||
|---|---|
|
||||
| `public/startupservice` | Once per session, so every job log carries the session context |
|
||||
| `editors/getdata` | The PRE_EDIT_HOOK |
|
||||
| `editors/stagedata` | The POST_EDIT_HOOK, through the loader |
|
||||
| `editors/restore` | The POST_EDIT_HOOK, through the loader |
|
||||
| `editors/getdynamiccolvals` | The dynamic cell dropdown programs |
|
||||
| `auditors/postdata` | The PRE_APPROVE_HOOK and POST_APPROVE_HOOK |
|
||||
| `editors/loadfile` | The POST_EDIT_HOOK - see the caveat below |
|
||||
|
||||
Those are the services that run customer code. The viewer's `viewlibs`, `viewtables` and `viewdata`, the metadata services and `usernav` receive neither table, so the high-frequency calls stay lean.
|
||||
|
||||
## Reading them in a hook
|
||||
|
||||
A hook script is `%include`d into the running service, so it shares the service's WORK library and can read the tables directly. Take an embedded editor opened at:
|
||||
|
||||
```
|
||||
https://datacontroller.io/#/editor/SASHELP.CLASS?labels=true&embed=va
|
||||
```
|
||||
|
||||
`browser_url_vars` then holds a row per parameter - `labels=true` and `embed=va` - and a hook can read either by name:
|
||||
|
||||
```sas
|
||||
%let labels=N;
|
||||
%let embed=N;
|
||||
|
||||
%if %sysfunc(exist(work.browser_url_vars)) %then %do;
|
||||
proc sql noprint;
|
||||
select value into :labels
|
||||
from work.browser_url_vars
|
||||
where name = 'labels';
|
||||
select value into :embed
|
||||
from work.browser_url_vars
|
||||
where name = 'embed';
|
||||
quit;
|
||||
%end;
|
||||
```
|
||||
|
||||
Or branch on what the user's browser reports:
|
||||
|
||||
```sas
|
||||
data _null_;
|
||||
set work.browser_info;
|
||||
call symputx('dc_timezone', timezone);
|
||||
call symputx('dc_locale', locale);
|
||||
run;
|
||||
```
|
||||
|
||||
Note the `exist()` guard. A service called directly, or by a frontend older than this feature, has no `work.browser_info` at all, so an unguarded `set` or `select` would fail the job. Guard first, then read.
|
||||
|
||||
## In the job log
|
||||
|
||||
Turn debug on and the session initialisation writes both tables to the log, so a ticket can be diagnosed without a second round of questions:
|
||||
|
||||
```
|
||||
NOTE: MPEINIT: work.browser_url_vars:
|
||||
name=embed value=va
|
||||
NOTE: MPEINIT: work.browser_info:
|
||||
url=... referrer=... timezone=Europe/Berlin tz_offset=-120 locale=en-GB
|
||||
dc_version=... adapter_version=... browser=Chrome browser_version=...
|
||||
platform=Linux user_agent=...
|
||||
```
|
||||
|
||||
The debug value depends on the path - `&_debug=131` on the Compute API and SAS 9, `&_debug=128` on the Viya web (JES) path with `runAsTask` enabled, which is what the frontend sends there. Either way, switching debug on in the app is enough.
|
||||
|
||||
## Worth knowing
|
||||
|
||||
- The values are supplied by the client. Treat them as diagnostics hints, never as a security boundary.
|
||||
- `editors/loadfile` is reached through the adapter's multipart file upload, which carries no input tables, so in normal use that service - and the hook it runs - does not receive them. It is on the list for completeness.
|
||||
- Nothing else pays for the tables: they travel only with the services above.
|
||||
|
||||
Full detail, including the column definitions and the log output, is in the [troubleshooting documentation](https://docs.datacontroller.io/dci-troubleshooting/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
A support ticket starts with questions. Which browser? Which timezone? Which build? What was in the URL?
|
||||
|
||||
Data Controller now answers them itself.
|
||||
|
||||
Two tables travel with the startup service, and with every service that runs your own code:
|
||||
|
||||
→ browser_info - one row: browser and version, platform, timezone and UTC offset, locale, the DC and adapter builds, the page URL and the referrer
|
||||
|
||||
→ browser_url_vars - one row per URL parameter, name and value, read straight into a macro variable
|
||||
|
||||
Both land in the job log when debug is on, so a ticket can be diagnosed from the log instead of a round trip. Both are readable from a hook script - %include shares the service's WORK library, so a hook can branch on the user's timezone, or pick up a parameter from the URL.
|
||||
|
||||
They are client supplied values: diagnostics hints, not a security boundary. Guard with %sysfunc(exist()) - a service called directly, or an older frontend, sends nothing.
|
||||
|
||||
✅ Full detail in the Data Controller documentation - link in the comments.
|
||||
|
||||
#sas #sasviya #datagovernance #datacapture #sasjs
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
Generate a 16:9 landscape illustration for a B2B data-governance blog cover, 1200x627.
|
||||
|
||||
Style: flat vector illustration, dark slate background (#314351) with a faint evenly spaced dot grid, brand green accent (#90c445), lighter slate panels (#2e4252), soft diffused shadows, subtle depth.
|
||||
|
||||
Scene: two rounded panels side by side, each with a header bar in brand green and four rows of rounded cells. The left panel carries short uniform value blocks (the browser context); the right panel carries name-and-value pairs, drawn as a narrow block beside a wider one (the URL parameters), with a single row in the left panel highlighted in green. Between and below the panels a thin vertical connector line runs down to a wide, flat log strip of alternating short green rules on a lighter slate panel, suggesting the tables being written to a job log.
|
||||
|
||||
Minimal text, no letters, no numbers, no logos, no watermark. Keep the subject inside the central square so a 1:1 crop is safe; the outer left and right thirds may be croppable background only.
|
||||
|
||||
Output: save as ./support-diagnostics.jpeg at 1200x627 (matching the other feed covers, and doubling as the LinkedIn share card). Do not also embed the image in the markdown body.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: 'Data Controller for SAS® - Who Is It For?'
|
||||
description: Five roles, five questions - what Data Controller for SAS® answers for administrators, auditors, managers, data owners and purchasers.
|
||||
date: '2026-09-26 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './who-is-data-controller-for.jpeg'
|
||||
---
|
||||
|
||||
# Data Controller for SAS® - Who Is It For?
|
||||
|
||||
Data Controller for SAS® gives business users a safe way to add, modify and delete data in SAS tables and databases, without handing them write access to production. That one capability answers a different question for each role that touches the data - so here is the short version, role by role.
|
||||
|
||||
## Data Administrators
|
||||
|
||||
Would you grant self-serve data updates in SAS if there was no risk to the data model? That is the trade Data Controller offers. End users never get direct write access to the target tables - every change is staged, validated at the point of entry, and applied by the service itself. Data Controller changes data, not structure, so your data model stays as it is.
|
||||
|
||||
## Data Auditors
|
||||
|
||||
Do you need to know who made a particular data change, what that change was, when it was made, and who signed it off? Every submission carries a full audit trail, and any associated Excel file is retained alongside it, so the source of a change can be reviewed long after it was loaded.
|
||||
|
||||
## Data Managers
|
||||
|
||||
Do you want to load, modify or delete production data without writing repetitive code, saving sensitive data to shared filesystems, building and deploying jobs, or crafting long-winded change requests? Adding a new editable table is configuration rather than development - the grid is generated from the target table's metadata, and the load type (composite or retained keys, SCD2, bitemporal) is picked from a list.
|
||||
|
||||
## Data Owners
|
||||
|
||||
Do you like to review data changes before they are applied? Approvers see the diff - new, changed and deleted rows - and nothing lands until it is signed off. Email alerts can tell you when data is changed or about to change, and a completed load can trigger a SAS job downstream. If the wrong change gets approved anyway, the table can be rolled back to a previous state - and the rollback is itself a new, reviewable change, so the audit trail keeps both the mistake and the correction.
|
||||
|
||||
## Purchasers
|
||||
|
||||
How about a business friendly, no lock-in software licence? The price list is public, every package includes unlimited users, and a perpetual licence is available. The Community package is free, so evaluating Data Controller costs nothing but the installation.
|
||||
|
||||
## What it is, in one line
|
||||
|
||||
Data Controller for SAS® is a robust solution for secure data uploads into any SAS-connected database, with full audit trail and approval mechanism. Configuration is quick, installation is easy, and everything runs on your own SAS platform with no external calls.
|
||||
|
||||
Multiple load types are supported (composite and retained keys, SCD2, bitemporal, format catalogs), data can be validated at the point of entry, and Excel end-user computing files can be dragged straight into SAS. Data Rollback, Data Catalog and Data Viewer come as standard.
|
||||
|
||||
Data Control.
|
||||
|
||||
Want to see it working on your own data? [Book a Discovery Call](/contact).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Data Administrators → would you grant self-serve data updates in SAS, if there was no risk to the data model?
|
||||
|
||||
Data Auditors → do you require oversight of who made a particular data change, what that change was, when it was made, and who signed it off? And to review the original excel, if used as a source?
|
||||
|
||||
Data Managers → do you prefer to load / modify / delete production data WITHOUT writing repetitive code, saving sensitive data to shared filesystems, building & deploying jobs, or crafting long-winded change requests?
|
||||
|
||||
Data Owners → do you like to review data changes before they are applied? How about receiving email alerts or the ability to set up job triggers after tables are updated?
|
||||
|
||||
Purchasers → how about a business friendly, no lock-in software licence?
|
||||
|
||||
Data Controller for SAS® is a robust solution for secure data uploads into any SAS-connected database, with full audit trail and approval mechanism.
|
||||
|
||||
Installation is easy and configuration is quick ⚡ Multiple load types supported (composite / retained keys, SCD2, Bitemporal, Format catalogs). Perform data validation at point of entry. Drag & drop Excel EUCs straight into SAS.
|
||||
|
||||
Data Rollback, Data Catalog, Data Viewer.
|
||||
|
||||
Data Control. ✅
|
||||
|
||||
#sas #sasviya #sasusers #datagovernance
|
||||
|
||||
https://lnkd.in/dunqWhnt
|
||||
|
||||
Image prompt:
|
||||
|
||||
Flat vector illustration, dark slate blue background (#314351), wide 1.91:1 banner (1200 x 627). Five simplified human figures stand in a shallow arc across the centre, each rendered in flat corporate style with minimal facial detail: an administrator beside a padlock, an auditor with a magnifying glass over a document, a manager holding a spreadsheet with an upward arrow, a data owner holding an approval tick, and a purchaser holding a price tag. Behind them, a translucent dashboard panel shows a data grid with one row highlighted as a pending change, and a small diff of new, changed and deleted rows. Thin connecting lines link each figure to the panel, suggesting that every role sees the same controlled workflow. Bright green accent (#90c445) used sparingly on the approval tick, the highlighted row and the connecting lines. Subtle depth with soft shadows, crisp edges. No text, no words, no letters, no logos, no watermarks. Keep the figures and the panel inside the central square so the image is safe to crop to 1:1 - the outer left and right thirds are background only.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 138 KiB |
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>One Table, Two Rule Sets has moved</title>
|
||||
<link rel="canonical" href="https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/" />
|
||||
<meta http-equiv="refresh" content="0; url=https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/" />
|
||||
<meta name="robots" content="noindex" />
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
This page has moved to
|
||||
<a href="https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/">docs.datacontroller.io</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||