Selecting it
The caller selects the mode, never the repository, and never the agent’s SQL..dex/config.yml cannot select the mode, in either direction. An agent may have
written that file, so a switch there would be a switch the agent controls. A
result_mode key in it is reported by dex config validate (kind
caller_only_key, not fatal) and rides every envelope as a warning, and
dex config set result_mode ... is refused. An unknown DEX_RESULT_MODE value
refuses every command (reason: configuration) rather than reading as full.
Every result produced in the mode says so: data.result_mode is
"values_free" on every CLI envelope the process prints, refusals and
needs_confirmation asks included, and Result.result_mode is
ResultMode.VALUES_FREE on every library result. A host asserts on it. Outside
the mode the key is absent, so a full-mode payload is unchanged.
Which commands run
An allowlist, so a command added in a later release refuses until someone has reviewed what it returns:connect test,config get,config validateexplore inventory,explore profile,explore relationships,explore map,explore diagramexplore query, including several statements,--sql-fileand--acrossexplore semantic list(it reads repository metadata, not the warehouse)
reason: guard and
data.code: command_not_values_free. That includes explore cluster (a small
cluster’s centroid is one row’s values), explore semantic values and
explore semantic query (their output is dimension values or group keys), every
transform, semantic and maintain command, config set and config unset,
and demo. On the library, every DexEngine method outside the reviewed set
raises ValuesFreeError with that code.
What profile keeps and withholds
Kept: row counts, null_fraction, distinct_count and distinct_count_exact,
is_unique, data_type, nullable, candidate keys, grain, composite keys,
key_evidence, findings, rank_score, PII category and confidence, temporal
granularity, span in days, period and gap counts, and every elision count.
Withheld: min_value, max_value and value_domain on every column, numeric
ones included, because a maximum salary is a salary. They are absent from each
column rather than null, named once per dataset in withheld_fields, and kept out
of suppressed_fields, whose meaning (“null on every column shown”) they do not
share. A column the default summary showed only because it had a value domain is
not shown in the mode, so which columns appear does not depend on what the cache
held.
The free-text notes (data_quality, notes, findings[].detail) are dex’s own
sentences and carry counts, fractions, column names and fixes, never a value. A
BigQuery batch the server refused while profiling is noted with BigQuery’s
structured reason code rather than its message, in every mode, because notes are
cached and copied into explore map.
map, relationships, inventory and diagram never carried a value; in the
mode they are stamped, and any profile they scan on the way is cached without
values. MapResult.cache, the cache handed back to a library caller, is a copy
with every value withheld.
What query accepts
Only a statement whose every output column is an aggregate over the rows it
reads, returning one row:
- One plain
SELECT. NoUNION,INTERSECTorEXCEPT(set_operation). - No
GROUP BYkeyed by a column (group_by_column): group keys come back as cells.GROUP BY ()and a string constant are fine;GROUP BY 1names a column by position, andGROUP BY ALL,ROLLUP,CUBEandGROUPING SETSkey on columns. - No
DISTINCT(distinct_projection),LIMIT,OFFSETorFETCH(limit_offset),ORDER BY(order_by),QUALIFY(qualify), or window function (window_function). - Every output column contains at least one aggregate on the allowlist
(
no_aggregateotherwise). The allowlist is the firewall’s measuring aggregates:COUNT(includingCOUNT(DISTINCT ...)),COUNT_IF/COUNTIF, the approximate distinct counts,SUM,AVG, theSTDDEVandVARIANCEfamily,CORR,COVAR_*,BOOL_AND,BOOL_OR,ENTROPY,KURTOSIS,SKEWNESS, and ClickHouse’suniq*family with its-If,-OrNull,-OrDefaultand-Distinctcombinators.FILTER (WHERE ...)is allowed. - Any other aggregate is refused (
aggregate_not_allowed):MIN,MAX,ANY_VALUE,ARG_MIN/ARG_MAX,MIN_BY/MAX_BY,MODE,FIRST/LAST,MEDIAN, every quantile and percentile,ARRAY_AGG,STRING_AGG,LISTAGG,LIST,APPROX_TOP_K,topK,groupArray, and anything not yet reviewed. - Outside its aggregates an output column may hold only numeric literals,
arithmetic, comparisons and boolean logic,
CASEwhose branches obey the same rule, a cast to a numeric type, andROUND,ABS,COALESCE,NULLIF,GREATEST,LEAST,CEIL,FLOOR,SQRT,POWER,LN,LOG,EXP,SIGN,SAFE_DIVIDE. A bare column isbare_column(so is*), a string, date or boolean literal isliteral_projection, any other function isfunction_not_allowed. - A scalar subquery in the select list must pass the same rule
(
unaggregated_subquery). Subqueries and CTEs inFROM, and everything inWHERE,HAVINGand join conditions, are unrestricted: values flow into them, not out of them.
multi_row_result), a cell that is not a number or a boolean
(non_numeric_result; DuckDB’s AVG over a timestamp is a timestamp), or a
result that would have been truncated to fit the payload budget
(would_truncate) is discarded, and the statement refused. Refuse, never
truncate: a host relies on a refusal being a refusal.
Several statements are judged one at a time, as always. A refused statement in a
batch has status: refused, its code, and no cells; its neighbors are
unaffected.
Errors carry codes, not words
A warehouse’s own error text can quote the value it failed on (Could not convert string 'abc' to INT32, invalid input syntax for type integer: "abc"). In the
mode, an error reaching the caller carries its class (reason) and a stable code
(data.code on the envelope, code on a batch statement, .code or
.values_free_code on the exception):
The replacement does not chain the original, so a traceback does not print the
words it exists to drop. The query ledger records the code in place of the words
too. It still records each statement’s SQL, which is the agent’s own text.
The cache
A profile written in the mode keeps no value: the entry is markedvalues_withheld, and a caller outside the mode treats it as stale and scans
again, so it is never served as a complete profile. A full profile already in the
cache is served to a caller in the mode with its values projected away, and is
left exactly as it was for the next full caller. Relationship inference runs
before values are withheld, so the confidences it reports are the same in either
mode; only a later run that reuses a withheld entry infers without its ranges.
The conformance suite
The guarantee ships with an executable check, so a host can run it against the release it pins:exmergo-dex-core[values-free-conformance] (pytest and DuckDB). The suite
builds a small DuckDB warehouse whose every text, date, timestamp and numeric
value carries a distinctive sentinel, then asserts that no sentinel appears in
any result, envelope, refusal, warehouse error, cache entry the mode wrote, or
ledger row; that every accepted statement returns numbers and every refused one
its code and no cell; that neither mode’s cache entries leak into the other; and
that no result carries a field it has not reviewed (KNOWN_FIELDS), so a field a
later release adds fails the suite until someone decides it is safe. Override
make_engine to build the engine the way your host does.
What it does not claim
These are residual risks the mode names rather than closes. A host bounds them with a cost ceiling and a query count per run.- Membership probing.
select count(*) from t where email = 'a@b.c'returns a number, and enough such probes test guesses about values. - Small counts. A count of one identifies a row.
- An aggregate over one row is that row’s value.
select avg(salary) from t where id = 42is accepted and returns a salary.
references/values-free-mode.md (edit on main). This page is generated; change the source, not the site.