pf_app
Go to file
Paul Trowbridge 766ccc52bb Scope an operation by a substring filter, case-insensitively
A view narrowed with `partd contains 'Keysto'` could be totalled but not
written: `SCOPE_OPS` carried the comparison and list operators and refused
everything else, so the panel showed a figure the operation answered 400 on.
Refusing is the right default -- a scope silently dropped is a write wider than
the panel that authorised it -- but `contains` is how a text dimension is
actually filtered, the alternative being an IN list of the 180 descriptions
that happen to match today.

ILIKE, not LIKE, because that is what the engine does: `t_tscalar::contains`
lowercases both sides before `std::string::find`, as do `begins_with` and
`ends_with`. On `fc_osm_skinny_34` the difference is 16,864 rows against 35, so
LIKE would have written a fortieth of what the grid displayed. Narrower is no
safer than wider here -- either way it is not the set the ledger counted.

The term is literal in the engine, so `%`, `_` and `\` are escaped out of it
before it becomes a pattern; `50%` means those three characters.

`matches` stays refused. It is RE2 in the engine and Postgres regexes are not
RE2 -- `\b` is a word boundary in one and a backspace in the other -- so a
pattern both accept could match a different set of rows, which is worse than a
400.

Every operator that takes a term now refuses an empty one. An unfinished filter
row in the pivot arrives as `[col, '==', null]`, which used to compile to
`= ''`; as a substring op it would have been `ILIKE '%%'`, which is every
non-null row and therefore no scope at all.

Runtime code, not template: nothing to regenerate, only a restart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:53:18 -04:00
lib Scope an operation by a substring filter, case-insensitively 2026-09-22 11:53:18 -04:00
public Add UX mockup and update spec with navigation direction 2026-04-25 14:56:29 -04:00
routes Manage a version's tags as a list, not one entry at a time 2026-09-22 09:00:27 -04:00
setup_sql Land on the forecast somebody chose, not the newest one 2026-09-22 08:34:46 -04:00
ui Manage a version's tags as a list, not one entry at a time 2026-09-22 09:00:27 -04:00
.env.example Put the app behind a login 2026-09-14 22:20:25 -04:00
.gitignore Refresh lockfiles and keep .env backups out of the tree 2026-09-16 17:45:58 -04:00
CLAUDE.md Scope an operation by a substring filter, case-insensitively 2026-09-22 11:53:18 -04:00
install.sh Put the app behind a login 2026-09-14 22:20:25 -04:00
package-lock.json Refresh lockfiles and keep .env backups out of the tree 2026-09-16 17:45:58 -04:00
package.json Put the app behind a login 2026-09-14 22:20:25 -04:00
PERSPECTIVE.md Filter aggregates with the rest of the layout 2026-09-19 14:43:00 -04:00
pf_perspective_options.md Record DuckDB virtual-server spike findings; choose display-grain pre-aggregation 2026-08-17 21:47:55 -04:00
pf_spec.md Merge feature/static-grain: display-grain pre-aggregation 2026-09-16 18:08:20 -04:00
pf.sh Fix the territory commands' success message and menu order 2026-09-18 11:47:02 -04:00
server.js Clone a version's definition without running it 2026-09-21 10:23:35 -04:00