Commit e79bf81
authored
fix(tables): refuse scale suffixes and resolve the lone-separator ambiguity (#6111)
* fix(tables): refuse scale suffixes and resolve the lone-separator ambiguity
A verification pass over the previous commit found that my own fix opened a new
hole of the class it closed.
Widening the currency marker to 1-3 letters made scale suffixes parse: `1.2 M`
read as 1.2, so a column of `1.2 M` / `3.4 M` — an ordinary spreadsheet paste —
converted cleanly and rewrote every cell a millionfold too small. Before the
widening those were rejected and the data was safe. Now refused explicitly,
while `kr` and `zł` still parse despite starting with the same letters.
Stripping the marker also newly routed formatted zero-decimal amounts into the
lone-separator branch, where a single dot was always decimal: `1.235 ¥` read as
1.235 rather than 1235. That is a wrong number where there used to be a
refusal, which is the worse failure. A lone separator followed by three digits
is now resolved by two signals — a marker means a formatter produced it, and
formatters group; a currency carrying three decimals (KWD, TND) reads them as
decimals. `coerce` passes the column's code, so the parser can ask. Bare typed
input keeps the decimal reading.
Also: the unchanged-type throw fell through to a 500 for every type except
currency, whose message happened to contain the word; it now maps to 400. And
the last three comments describing the removed two-transaction architecture are
gone — the previous commit claimed they were and two survived.
Documented, and failing closed rather than guessing: markers written flush
against the digits (`Rp12,00`) stay rejected, because a letter touching a digit
is the only thing distinguishing a currency marker from a part number, and
reading `SKU400` as 400 invents a value where refusing merely inconveniences.
* fix(tables): fit the whole column-type list in the New column dropdown
Adding currency made the list nine items (295px with its separator and
padding), past the shared dropdown's 240px cap — so Select and Workflow sat
below a scrollbar.
Sized on this dropdown rather than on the shared `DropdownMenuContent`, which
every other menu in the app relies on. Also gave the sidebar's type combobox
headroom, since it gained the same entry and was within 4px of its own cap.
* fix(tables): read a lone separator against the column's currency
Review of the previous commit found the marker signal alone was too coarse.
The evidence: a currency with decimal places ALWAYS formats with both
separators — `1.234,56 €`, `$1,234.56`, `1.234,000 KWD`. Only a zero-decimal
currency emits a lone one (`1.235 ¥`). So a marker does not imply grouping; the
currency's own decimal places decide, and a three-decimal currency's trailing
three digits are decimals however the value arrived.
That matters most for CSV import, which passed no currency at all: a KWD column
importing `0,500` read as five hundred rather than a half. The column's code is
now forwarded, since nothing in a CSV carries a marker to fall back on.
A bare typed `1.234` still reads as decimals.
* fix(tables): let the separator decide a lone-separator amount, not the marker
Greptile blocked on `$1.234` reading as 1234, and offered the alternative of
stating the input contract explicitly. Looking for a precedent settled it
against me: a spreadsheet with a USD-formatted cell stores a typed `1.234` as
1.234 and displays $1.23. Reinterpreting it as one thousand two hundred
thirty-four is a thousandfold surprise, and my defense of it was wrong.
The rule now keys on the separator, tempered by what the currency can express:
- A dot is the decimal point in the notation most people type, so it stays a
decimal — except for a currency with no decimal places, where `1.235 ¥`
cannot be a fraction of a yen and is the one lone-separator form a formatter
actually emits.
- A comma is grouping by convention (`1,500` is fifteen hundred), except for a
three-decimal currency, where `0,500` is a half.
A currency with one or two decimal places always formats with both separators,
so no formatter output is decided here at all.
This also drops the `hadMarker` signal entirely — once the separator and the
currency answer the question, where the string came from stops mattering.
* fix(tables): check conversions against the target column, not a rebuilt stub
Bugbot flagged that the conversion gate calls `isValueCompatibleWithType`,
which hand-builds a column from loose arguments and never sets
`currencyCode` — while the coercion a few lines later reads the real one.
The reported impact does not occur. Whether an amount parses does not depend
on the currency, only which number it yields, so the gate and the coercion
always agree on accept/reject; and the value written back is already coerced
against the real column, so no cell is stored wrong. I checked this rather
than argued it: across nine currency codes and every separator shape in the
parser's repertoire, acceptance never diverged.
The stub is still worth removing. It is correct only because of an invariant
nobody wrote down, and `convertedColumn` — sitting right there, whose own
comment already says it exists so the scan reads the same option set and
currency — is what the gate should have used all along.
So the gate now takes the target column itself. The loose-argument form moves
into the test file as local marshalling, leaving production with only the
whole-column shape, which cannot silently drop a key a future type adds.
No behavior change, so no test changes: a currency-divergence regression test
could not fail today, and pinning an invariant this removes the need for would
only block a legitimate future change.
* fix(tables): vet a currency marker against an allowlist, not a scale denylist
Bugbot found that `SCALE_SUFFIX` misses `mio`, `mrd`, `bio`, `tsd`, `mln`
and `md`. Each is one to three letters, so each matched the currency-marker
pattern and was stripped: `1,2 mio` read as 1.2, a millionfold too small,
which is the same failure `1.2 M` had.
Adding the six words would leave the next six. The denylist is the defect —
it has to enumerate every magnitude abbreviation in every language, and each
one it misses silently rescales a value rather than refusing it.
So the check is inverted. A bare letter token is stripped only when it names
a currency: an ISO 4217 code the runtime can enumerate, or one of the common
non-ISO markers people type (`kr`, `zł`, `Kč`). Anything else is left in
place, fails the amount-shape check, and is refused. `12 units` and `12 pcs`
now fall out of the same rule the magnitude words do, without naming either.
A marker carrying a currency symbol skips the check — no magnitude
abbreviation contains one — so `R$`, `CHF`, `1 234,56 kr` are untouched.
This rests on ISO codes and magnitude words being disjoint, which is true for
all 162 codes the runtime knows, and is now pinned by a test so a future
collision fails loudly instead of silently.
`SCALE_SUFFIX` survives only for a runtime without `Intl.supportedValuesOf`,
which cannot enumerate codes and so keeps the old permissive behaviour;
rejecting every letter marker there would break `USD 12.50`. It no longer has
to be complete, since every modern runtime rejects by non-recognition.
Verified the new tests fail when the vetting is reverted.
* fix(tables): group a lone dot only when a zero-decimal amount came formatted
Greptile found that a bare `1.235` typed into a JPY column stored 1235. The
zero-decimal branch read the dot as grouping without asking where the value
came from, so typing, pasting, a tool write, or an import all inflated a
thousandfold in silence.
The rule was inverted relative to its own evidence. Grouping was justified as
"the one lone-separator form a formatter emits" — and a formatter always emits
its marker too. The marker is exactly what separates formatter output from
someone typing, and I had removed it as redundant two commits ago.
So a lone dot groups only for a zero-decimal currency AND only when a marker
came with it. `1.235 ¥` and `JPY 1.235` still read as 1235; a bare `1.235`
now stores 1.235 and displays `¥1` — wrong in a way the writer can see and
correct, rather than a silent thousandfold error.
This does not walk back the earlier fix. For a currency with one or two
decimal places a formatter always emits BOTH separators, so a lone dot there
never came from one and stays a decimal whether or not a marker is present —
`$1.234` is still 1.234. The marker only carries information in the
zero-decimal case, which is the only case that now consults it.
The two overlapping lone-separator tests are replaced by three that each state
one rule. Verified the new one fails when the marker requirement is reverted.1 parent ef00750 commit e79bf81
11 files changed
Lines changed: 297 additions & 67 deletions
File tree
- apps/sim
- app
- api
- table/[tableId]/columns
- v1/tables/[tableId]/columns
- workspace/[workspaceId]/tables/[tableId]/components
- column-config-sidebar
- new-column-dropdown
- lib/table
- __tests__
- column-types
- columns
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
323 | 323 | | |
324 | 324 | | |
325 | 325 | | |
326 | | - | |
| 326 | + | |
| 327 | + | |
327 | 328 | | |
328 | 329 | | |
329 | 330 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
371 | | - | |
| 371 | + | |
| 372 | + | |
372 | 373 | | |
373 | 374 | | |
374 | 375 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
282 | 282 | | |
283 | 283 | | |
284 | 284 | | |
285 | | - | |
| 285 | + | |
286 | 286 | | |
287 | 287 | | |
288 | 288 | | |
| |||
Lines changed: 5 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
90 | 94 | | |
91 | 95 | | |
92 | 96 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
16 | 37 | | |
17 | 38 | | |
18 | 39 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
163 | 236 | | |
164 | 237 | | |
165 | 238 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
749 | 749 | | |
750 | 750 | | |
751 | 751 | | |
752 | | - | |
753 | | - | |
754 | | - | |
755 | | - | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
756 | 755 | | |
757 | 756 | | |
758 | 757 | | |
759 | 758 | | |
760 | | - | |
| 759 | + | |
761 | 760 | | |
762 | 761 | | |
763 | 762 | | |
| |||
802 | 801 | | |
803 | 802 | | |
804 | 803 | | |
805 | | - | |
806 | | - | |
807 | | - | |
808 | | - | |
809 | | - | |
810 | | - | |
811 | | - | |
812 | | - | |
813 | | - | |
| 804 | + | |
814 | 805 | | |
815 | 806 | | |
816 | 807 | | |
| |||
1331 | 1322 | | |
1332 | 1323 | | |
1333 | 1324 | | |
1334 | | - | |
1335 | | - | |
1336 | | - | |
1337 | | - | |
1338 | | - | |
| 1325 | + | |
| 1326 | + | |
| 1327 | + | |
| 1328 | + | |
| 1329 | + | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
| 1334 | + | |
| 1335 | + | |
| 1336 | + | |
1339 | 1337 | | |
1340 | 1338 | | |
1341 | 1339 | | |
1342 | 1340 | | |
1343 | | - | |
1344 | | - | |
1345 | | - | |
1346 | | - | |
1347 | | - | |
1348 | | - | |
1349 | | - | |
| 1341 | + | |
1350 | 1342 | | |
1351 | 1343 | | |
1352 | | - | |
1353 | | - | |
1354 | | - | |
1355 | | - | |
1356 | | - | |
1357 | | - | |
1358 | | - | |
| 1344 | + | |
1359 | 1345 | | |
0 commit comments