11 KiB
Where the web UI disagreed with the server
Thirteen places where the Add-table builder and the append-columns panel offered,
promised or accepted something the backend does not do. All thirteen are fixed in
the web crate; the server is untouched. Ordered by severity.
Severity is what the gap costs the user:
- Blocking — the UI leads you to a definition the server refuses outright.
- Wrong — the UI states something untrue about the table you are creating.
- Missing — a real backend capability the UI gives you no way to reach.
- Misleading — accurate enough to act on, wrong enough to confuse.
cargo test -p web: 158 passed, up from 134. Clippy clean for every file
touched.
1. Indexing a FKlink — blocking
Was: the column panel offered Indexing: yes/no for a link like any other
column, and the column list gave it an index toggle.
Reality: a link is a foreign key, and the server builds an index for every
one of them as it creates the table
(server/src/table_definition/managed_table.rs:361). Naming a link in indexes
is not redundant — it is refused:
Link 'billing_customer' is indexed automatically
post_table_definition.rs:514, and the same rule on the append path at
add_table_columns.rs:322. So the whole table failed to create, after the
Create button, for a checkbox the UI itself offered.
Now: ColumnDraft::is_indexable is false for a link, the Indexing field is
replaced by "Indexed automatically — a link is a foreign key, and the server
indexes every one of them", the column list reports indexed automatically
with no toggle, selected_index_names can never name a link, and a rebuilt or
tampered post is refused here with the server's own reason instead of being
forwarded. Both screens.
2. Global tables and the columns that post to a profile's books — blocking
Was: the shared scope offered ACCOUNTING, ACCOUNTING_TRANSFER and the quantity-ledger switch exactly as a profile scope does.
Reality: post_table_definition.rs:280 — "Global tables cannot use
accounting or quantity-ledger columns". A shared table belongs to every profile
at once, so there is no one set of books for it to post to.
Now: the two types are not offered in the shared scope, the quantity-ledger field is not rendered there, and both are refused with the reason. Switching an existing draft to the shared scope reports it at the switch rather than at save — the picker going quiet is not an explanation on its own.
3. Table names could be 63 characters — blocking
Was: validate_identifier(…, "Table name"), the plain 63-character
identifier rule.
Reality: a table name is a prefix that longer identifiers are built from —
idx_<table>_<column>_fk has to fit in 63 too — so the server's limit is
38 (catalog/object_naming.rs:35). Anything from 39 to 63 characters passed
the browser and failed at the backend.
Now: validate_table_name, with the limit derived from the same arithmetic
and the same common constant the server reads, so a system column added there
moves it here too. The test asserts it lands on 38.
4. Reserved table names — blocking
Was: nothing stopped a table called accounts or general_ledger.
Reality: every profile is given general_ledger, journal_lines,
quantity_ledger, accounts and custom_exchange_rates when it is created;
post_table_definition.rs:221 refuses the names.
Now: refused in the builder, naming the table and why.
5. required was unreachable — missing
Was: proto_columns hardcoded required: false. No control anywhere.
Reality: required is a live column property. The server stores it and
enforces it on every row written
(table_validation/runtime.rs:75) — a row that omits a required column is
rejected. The web UI could not produce a required column at all, and could not
tell you a column was one.
Now: a Required field on both column panels, carried through the form,
listed as a required tag, shown in the preview and sent on the request.
6. The append screen could not link to a shared table — missing
Was: its link-target picker read page.tables, which for a profile scope is
that profile's own tables.
Reality: the server resolves a link against the profile's tables or any
global one (post_table_definition.rs:684). Shared tables — currencies, code
lists — were simply absent from the picker with no way to reach them.
Now: link targets are their own list, built by table_scope::linkable_tables
(the same helper the builder uses), while the browse list stays the scope's own
tables. This is the same class of bug as the "No table chosen" one fixed earlier
in this session: one question, three answers.
7. A link could point at the table being created — blocking
Was: the builder filtered its own name out of the picker, and that was all.
The table-name field has no hx-trigger by design, so adding link(invoice)
and then naming the table invoice sailed through.
Reality: post_table_definition.rs:400 — "Link 'x' cannot point at the
table being created".
Now: a rule in validate, not just a filter on a list, so the order the
fields were filled in cannot get round it.
8. link(accounts) was refused only by the server — blocking
Was: the builder's loader dropped accounts from the picker, but nothing
validated a draft that carried the link anyway.
Reality: "The account relationship is built into ACCOUNTING and cannot be
declared as a link" (post_table_definition.rs:406).
Now: a rule in validate, shared by both screens.
9. A declared column could collide with a generated one — blocking
Was: duplicate names were checked against the declared columns only.
Reality: the server checks against every name the table will hold, generated
ones included — "Column 'debit' conflicts with a column generated for
ACCOUNTING" (models.rs:450). Declaring debit beside an ACCOUNTING row was
accepted by the builder and refused by the backend.
Now: ColumnDraft::claimed_names is the set both add and validate check,
in both directions — the declared column first or the definition row first.
Companions named after their own column (work_phone_ext) still never collide.
10. Rows could not be identified by a generated column — missing
Was: row-display candidates were the declared, non-compound columns.
Reality: the server accepts any of the table's real column names, generated
ones included (post_table_definition.rs:482), and even maps a display column
of accounting onto whatever its name companion ended up called. A table whose
only readable column is generated — an ACCOUNTING row's name — had nothing to
be shown by.
Now: every column the table will really hold is a candidate, under its aliased name, and the definition row itself still is not one.
11. The preview omitted row_revision — wrong
Was: the preview claims to be "the schema as it will exist" and listed id,
deleted, … created_at.
Reality: every managed table also carries row_revision
(common/src/system_column.rs:20). It was also missing from the names an alias
may not take, so row_revision could be asked for as a generated column's alias
and refused by the server.
Now: in the preview and in the reserved set.
12. "already exists in profile ``" — misleading
Was: the duplicate-table-name error always named a profile. In the shared scope there is no profile, so it rendered an empty pair of backticks.
Now: scope-aware. The shared scope says the name has to be free in every profile; a profile scope says the name may also be taken by a shared table, which is what the check really covers.
13. Index and options were reported twice — misleading
Was: option_label() included indexed, and the column list rendered it as
a tag beside the Indexed column that said the same thing.
Now: the list's Indexed column is the only place index state is reported; the options column carries required, quantity ledger and currency.
Deliberately left alone
- The link's
<n>_versioncolumn. Every link creates one (managed_table.rs:324), and the preview does not show it. It is an internal column —common/src/system_column.rssays so — never exposed by the data API, so listing it would be noise rather than honesty. account_idshown as well asaccount. Both are real: one is the physical column, one is the API name. The preview already explains the relationship.
Suggested server changes
None of these are needed for the fixes above — the web crate works around each one — but each is a place where a client can only be correct by knowing something the backend never tells it.
-
ListColumnTypesshould say which types a shared table may not use. Aprofile_only: boolonColumnTypewould let a picker be right without naming ACCOUNTING and ACCOUNTING_TRANSFER in client code. Todayweb/src/schema/mod.rshas to hardcode the pair (PROFILE_ONLY_TYPES), and a third type with the same restriction would be silently offered. -
ListColumnTypesshould say a link is indexed automatically. Same shape of problem as (1): the rule is real and client-visible, but the only way to know it is to have readmanaged_table.rs. Anindexed_automatically: boolwould make finding #1 impossible to reintroduce in any client. -
The reserved table names should be reportable.
general_ledger,journal_lines,quantity_ledger,accountsandcustom_exchange_ratesare constants of three separate server modules. Every client that wants to refuse them early has to copy the list, and will drift when the server adds one. They belong on a capability or catalog response. -
MAX_TABLE_NAME_LENGTHshould be reportable too, for the same reason. The web crate now derives it from the samecommonconstant the server does, which holds only because both compile the same source file — a coincidence of layout rather than a contract. -
Projection columns are unreachable from any client.
post_table_definitionhandles afrom(link_column.source_column)field type (link_column_projection.rs:30), butCOLUMN_TYPE_CATALOGdoes not report it andColumnTypeSpellinghas no variant for it — onlyBare,DecimalandLink. So a feature that exists in the backend cannot be offered by the web UI or the TUI. Either add the spelling and catalog entry, or the code is dead. -
The append path checks the global quantity-ledger rule only at execute time.
add_table_columns.rs:238runs after the table is resolved, so a client has to work out for itself whether the table it is appending to is global. The web crate now infers it from the profile tree; reporting it on the table would be sturdier.