Skip to content

feat: BROWSE per-cell validation by declared column type (#45) - #49

Merged
DDecoene merged 1 commit into
release/v1.2.0from
feature/45-browse-cell-validation
Jul 9, 2026
Merged

feat: BROWSE per-cell validation by declared column type (#45)#49
DDecoene merged 1 commit into
release/v1.2.0from
feature/45-browse-cell-validation

Conversation

@DDecoene

@DDecoene DDecoene commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Closes #45.

Summary

BROWSE now validates each cell edit against its column's declared type before committing. An invalid edit keeps the cell in edit mode, outlined red, with the reason shown (HH:MM, multiple of 15, at most 2 decimal place(s), not a real date); the message clears as soon as the value becomes valid, and Esc abandons the edit leaving the original value intact.

The rules live in one place — src/shared/cellValidation.ts — and run on both sides:

  • Grid.ts checks before commit, for instant feedback (the UX ask).
  • Session's grid-edit handler re-checks authoritatively before writing.

That second half turned out to be necessary, not just belt-and-braces: grid-edit bypassed the Executor entirely and wrote straight to SQLite with no validation whatsoever, so a WS message could plant any value in any column. The TIME check added in #43 only ever covered REPLACE.

Type Accepted
DATE YYYY-MM-DD, a real calendar date
TIME / TIME(n) HH:MM; minutes a multiple of n
NUM(p,s) numeric; ≤ s decimals, ≤ p - s integer digits
INT a whole number
LOGICAL .T./.F./.TRUE./.FALSE./T/F/TRUE/FALSE/1/0
CHAR / MEMO anything

Empty clears a cell and is always allowed. Columns with no recorded declared type stay unconstrained (so pre-existing tables behave exactly as before).

Supporting this meant recording the declared type of every column, not just TIME: SQLite's affinity can't tell TIME/DATE/CHAR apart (all TEXT), nor LOGICAL from INT (both INTEGER), nor recover a NUM(p,s) qualifier. CREATE TABLE now records all of them, grid-open ships a columnTypes map to the client, and LIST STRUCTURE prints the declared type (NUM(8,2), TIME(15)).

Two bugs found and fixed along the way

  1. NUM(p,s) created a phantom column. CREATE TABLE t (price NUM(8,2)) produced a column literally named 2 of type ) — the parser read the precision, then parsed the scale as the next column definition. This is live: the shipped PRODUCTS, DEALS and SALES demo tables all carry the stray column (LIST STRUCTURE on the inventory demo shows it). Fixed in parseCreate/skipTypeSize. Tables created before this fix keep the stray column until recreated.

  2. Column metadata leaked across databases. ColumnMetaStore (added in TIME column type #43) keyed on (table, column) with no database scope, so two databases with same-named tables overwrote each other's declared types — a TIME(15) column in one DB could be validated against another DB's CHAR(20) of the same name. Now keyed by (db, table, column), with a migration for the pre-BROWSE per-cell validation hook #45 schema (covered by a test). IndexStore has the same unscoped-key shape; that's pre-existing and left alone here.

Scope notes

  • REPLACE still enforces only TIME, exactly as TIME column type #43 shipped it. Widening it to DATE/NUM/LOGICAL would change the semantics of existing programs and demos, and the issue scopes the new validation to the grid. Noted in CLAUDE.md.
  • ALTER TABLE ADD/ALTER still drops the (n)/(p,s) qualifier (pre-existing skipTypeSize() behavior for every type); it now records the base type. skipTypeSize was taught to consume the ,s so it doesn't corrupt the token stream.

Assistant parity

No new command — this is BROWSE behavior, and the Assistant's existing Browse action opens the same grid. Per the DoD's Assistant-parity rule, the Assistant path is exercised in a real browser: a new tests/assistant.spec.ts case clicks Browse, types an off-granularity TIME(15) value, asserts the inline rejection, then commits a valid one.

Test plan

Suites run serially (they share data/).

  • npx tsc --noEmit clean.
  • npm test316/316 vitest (was 283). New: CellValidation.test.ts (18, the shared rules), ColumnMeta.test.ts (10, NUM(p,s) parsing, no phantom column, declared types in LIST STRUCTURE, grid-open.columnTypes, server-side grid-edit rejection, cross-database isolation), ColumnMetaStore.test.ts (5, per-(db,table,column) storage + legacy-schema migration).
  • npx playwright test79/79 (was 75). New tests/grid-validation.spec.ts (3): invalid TIME(15) rejected inline then valid one commits and lands in the DB; bad NUM(8,2) and impossible DATE rejected while CHAR accepts anything; Esc abandons an invalid edit and restores the original. Plus the Assistant Browse-action case above.

Docs

CHANGELOG.md (Added + Fixed), README.md (column types, BROWSE cell-validation note), CLAUDE.md (architecture tree incl. cellValidation.ts, cell-validation table, roadmap, test trees and counts).

Grid edits are now checked against the column's declared type before
they commit. An invalid edit keeps the cell in edit mode, outlined in
red, showing why; the error clears as soon as the value becomes valid,
and Esc abandons the edit leaving the original value intact.

The rules live in src/shared/cellValidation.ts and run on both sides:
Grid.ts for instant feedback, and Session's grid-edit handler as the
authoritative check. grid-edit previously bypassed the Executor and
wrote straight to SQLite with no validation at all, so a WS message
could plant any value in any column.

Serving that needed the declared type of every column, not just TIME:
SQLite's affinity cannot tell TIME/DATE/CHAR apart (all TEXT), nor
LOGICAL from INT (both INTEGER), nor recover a NUM(p,s) qualifier.
CREATE TABLE now records all of them, grid-open ships them to the
client, and LIST STRUCTURE prints them as declared.

Two bugs surfaced and are fixed here:

- CREATE TABLE t (price NUM(8,2)) created a phantom column named "2"
  of type ")" — the parser read the precision and then treated the
  scale as the next column definition. The PRODUCTS, DEALS and SALES
  demo tables all carried the stray column.

- Column metadata was keyed by (table, column) with no database
  scope, so same-named tables in different databases overwrote each
  other's declared types. Now keyed by (db, table, column), with a
  migration for the pre-#45 schema.
@DDecoene
DDecoene merged commit e9a23e4 into release/v1.2.0 Jul 9, 2026
2 checks passed
@DDecoene
DDecoene deleted the feature/45-browse-cell-validation branch July 9, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant