decimal precision constrained

This commit is contained in:
Filipriec
2026-08-24 19:58:44 +02:00
parent 60aca940ee
commit 16670f4375
13 changed files with 147 additions and 25 deletions

View File

@@ -212,6 +212,15 @@ td-precision = Přesnost
td-precision-hint = Celkový počet uložených číslic, alespoň 1.
td-scale = Desetinná místa
td-scale-hint = Číslice za desetinnou čárkou, nejvýše tolik jako přesnost.
td-fixed-decimal-access = Pokročilé číselné typy
td-fixed-decimal-disabled = Pouze doporučené typy
td-fixed-decimal-enabled = Povolit desetinné číslo s pevným měřítkem
td-fixed-decimal-access-hint = Doporučuje se přesný typ NUMERIC. Tuto možnost povolte pouze pro přísný externí datový kontrakt.
td-fixed-decimal-warning-title = Pokročilé desetinné číslo s pevným měřítkem
td-fixed-decimal-warning = Tento typ omezuje celkový počet číslic i desetinná místa. Hodnoty mimo měřítko se odmítnou, výpočty musí zaokrouhlovat explicitně a typ po vložení dat nelze změnit.
td-fixed-decimal-enable-first = Před použitím tohoto typu povolte pokročilé desetinné číslo s pevným měřítkem.
td-exact-decimal = Desetinné číslo — přesné (doporučeno)
td-fixed-decimal-type = Desetinné číslo s pevným měřítkem — pokročilé
td-referenced-table = Odkazovaná tabulka
td-choose-table = Vyberte tabulku
td-group-global = Globální

View File

@@ -216,6 +216,15 @@ td-precision = Precision
td-precision-hint = Total digits stored, at least 1.
td-scale = Scale
td-scale-hint = Digits after the point, no more than the precision.
td-fixed-decimal-access = Advanced number types
td-fixed-decimal-disabled = Recommended types only
td-fixed-decimal-enabled = Enable fixed-scale decimal
td-fixed-decimal-access-hint = Exact NUMERIC is recommended. Enable this only for a strict external data contract.
td-fixed-decimal-warning-title = Advanced fixed-scale decimal
td-fixed-decimal-warning = This type limits total and fractional digits. Values outside the declared scale are refused, calculations must round explicitly, and the type cannot be changed after the table contains data.
td-fixed-decimal-enable-first = Enable advanced fixed-scale decimal before using this type.
td-exact-decimal = Decimal — exact (recommended)
td-fixed-decimal-type = Fixed-scale decimal — advanced
td-referenced-table = Referenced table
td-choose-table = Choose a table
td-group-global = Global

View File

@@ -212,6 +212,15 @@ td-precision = Presnosť
td-precision-hint = Celkový počet uložených číslic, aspoň 1.
td-scale = Desatinné miesta
td-scale-hint = Číslice za desatinnou čiarkou, najviac toľko ako presnosť.
td-fixed-decimal-access = Pokročilé číselné typy
td-fixed-decimal-disabled = Iba odporúčané typy
td-fixed-decimal-enabled = Povoliť desatinné číslo s pevnou mierkou
td-fixed-decimal-access-hint = Odporúča sa presný typ NUMERIC. Túto možnosť povoľte iba pre prísnu externú dátovú zmluvu.
td-fixed-decimal-warning-title = Pokročilé desatinné číslo s pevnou mierkou
td-fixed-decimal-warning = Tento typ obmedzuje celkový počet číslic aj desatinné miesta. Hodnoty mimo mierky sa odmietnu, výpočty musia zaokrúhľovať explicitne a typ sa po vložení údajov nedá zmeniť.
td-fixed-decimal-enable-first = Pred použitím tohto typu povoľte pokročilé desatinné číslo s pevnou mierkou.
td-exact-decimal = Desatinné číslo — presné (odporúčané)
td-fixed-decimal-type = Desatinné číslo s pevnou mierkou — pokročilé
td-referenced-table = Odkazovaná tabuľka
td-choose-table = Vyberte tabuľku
td-group-global = Globálne

View File

@@ -61,6 +61,8 @@ pub(crate) struct BuilderForm {
#[serde(default)]
pub decimal_scale_input: String,
#[serde(default)]
pub fixed_decimal_input: String,
#[serde(default)]
pub column_indexing_input: String,
#[serde(default)]
pub column_quantity_ledger_input: String,
@@ -128,6 +130,7 @@ impl BuilderForm {
link_table_input: self.link_table_input.clone(),
decimal_precision_input: self.decimal_precision_input.clone(),
decimal_scale_input: self.decimal_scale_input.clone(),
fixed_decimal_input: self.fixed_decimal_input.clone(),
indexing_input: self.column_indexing_input.clone(),
quantity_ledger_input: self.column_quantity_ledger_input.clone(),
required_input: self.column_required_input.clone(),

View File

@@ -245,11 +245,18 @@ mod tests {
assert!(html.contains("Every profile can use this shared table."));
}
/// Every type the server accepts has to be reachable from the picker, or
/// the web UI silently offers less than the backend does.
/// Fixed-scale decimal is the one intentionally gated type; enabling the
/// advanced control makes it reachable without removing server support.
#[test]
fn the_type_picker_offers_the_parameterised_and_interval_types() {
let html = render_builder(&page());
let mut state = page();
let html = render_builder(&state);
assert!(!html.contains(r#"<option value="decimal""#));
assert!(html.contains(r#"name="fixed_decimal_input""#));
state.draft.columns.fixed_decimal_input = "yes".to_string();
let html = render_builder(&state);
for column_type in ["decimal", "duration", "period", "accounting"] {
assert!(
@@ -273,10 +280,12 @@ mod tests {
assert!(render_builder(&state).contains(r#"name="gtin_type_input""#));
// Decimal reveals its precision and scale.
state.draft.columns.fixed_decimal_input = "yes".to_string();
state.draft.columns.type_input = "decimal".to_string();
let html = render_builder(&state);
assert!(html.contains(r#"name="decimal_precision_input""#));
assert!(html.contains(r#"name="decimal_scale_input""#));
assert!(html.contains("Advanced fixed-scale decimal"));
// Money reveals its currency and rounding inputs.
state.draft.columns.type_input = "money".to_string();

View File

@@ -475,6 +475,10 @@ pub(crate) struct ColumnDraft {
pub link_table_input: String,
pub decimal_precision_input: String,
pub decimal_scale_input: String,
/// Explicit opt-in for the fixed-scale decimal type. It is intentionally
/// absent from the normal type picker because PostgreSQL coerces values to
/// its declared scale and populated tables cannot later change the type.
pub fixed_decimal_input: String,
pub indexing_input: String,
pub quantity_ledger_input: String,
pub required_input: String,
@@ -528,6 +532,7 @@ impl ColumnDraft {
required_input: "no".to_string(),
rounding_input: "none".to_string(),
currency_input: "EUR".to_string(),
fixed_decimal_input: "no".to_string(),
catalog,
..Self::default()
}
@@ -536,7 +541,15 @@ impl ColumnDraft {
/// The types this panel offers, which is the only place the creation-only
/// rule shows up in the markup.
pub(crate) fn offered_types(&self) -> Vec<String> {
self.catalog.offered_types(self.creating_table, self.global)
self.catalog
.offered_types(self.creating_table, self.global)
.into_iter()
.filter(|column_type| column_type != "decimal" || self.fixed_decimal_enabled())
.collect()
}
pub(crate) fn fixed_decimal_enabled(&self) -> bool {
self.fixed_decimal_input.trim().eq_ignore_ascii_case("yes")
}
pub(crate) fn temporal_types(&self) -> Vec<String> {
@@ -639,6 +652,9 @@ impl ColumnDraft {
return Ok(None);
}
if self.catalog.is_parameterised(&column_type) {
if !self.fixed_decimal_enabled() {
return Err(crate::tr!(locale, "td-fixed-decimal-enable-first"));
}
let precision = self.decimal_precision_input.trim();
let scale = self.decimal_scale_input.trim();
if precision.is_empty() && scale.is_empty() {
@@ -1442,6 +1458,8 @@ pub(crate) struct ColumnForm {
#[serde(default)]
pub decimal_scale_input: String,
#[serde(default)]
pub fixed_decimal_input: String,
#[serde(default)]
pub column_indexing_input: String,
#[serde(default)]
pub column_quantity_ledger_input: String,
@@ -1487,6 +1505,7 @@ impl ColumnForm {
link_table_input: self.link_table_input.clone(),
decimal_precision_input: self.decimal_precision_input.clone(),
decimal_scale_input: self.decimal_scale_input.clone(),
fixed_decimal_input: self.fixed_decimal_input.clone(),
indexing_input: self.column_indexing_input.clone(),
quantity_ledger_input: self.column_quantity_ledger_input.clone(),
required_input: self.column_required_input.clone(),
@@ -1755,7 +1774,10 @@ pub(crate) mod tests {
assert!(offered.contains(&"numeric".to_string()));
assert!(offered.contains(&"account".to_string()));
assert!(offered.contains(&"accounting_transfer".to_string()));
assert!(offered.contains(&"decimal".to_string()));
assert!(!offered.contains(&"decimal".to_string()));
let mut advanced = draft();
advanced.fixed_decimal_input = "yes".to_string();
assert!(advanced.offered_types().contains(&"decimal".to_string()));
// Families are one choice, resolved by a follow-up field.
assert!(offered.contains(&"temporal".to_string()));
assert!(offered.contains(&"gtin".to_string()));
@@ -1803,6 +1825,7 @@ pub(crate) mod tests {
assert_eq!(draft.added[1].data_type, "gtin_13");
draft.name_input = "weight".to_string();
draft.fixed_decimal_input = "yes".to_string();
draft.type_input = "decimal".to_string();
assert_eq!(draft.canonical_type_input(crate::i18n::Locale::default()).unwrap(), None);
assert!(draft.show_decimal_arguments());
@@ -1843,6 +1866,7 @@ pub(crate) mod tests {
fn decimal_arguments_follow_the_servers_rules() {
let mut draft = draft();
draft.name_input = "weight".to_string();
draft.fixed_decimal_input = "yes".to_string();
draft.type_input = "decimal".to_string();
for (precision, scale) in [("0", "0"), ("3", "5"), ("-2", "1"), ("08", "2"), ("4.5", "1")] {
@@ -1946,6 +1970,7 @@ pub(crate) mod tests {
// A parameterised decimal counts, through its head.
draft.name_input = "quantity".to_string();
draft.fixed_decimal_input = "yes".to_string();
draft.type_input = "decimal".to_string();
draft.decimal_precision_input = "12".to_string();
draft.decimal_scale_input = "3".to_string();

View File

@@ -127,13 +127,22 @@
{% if page.draft.columns.show_link_target() %}<small>{{ nav.tr("td-link-alias-hint") }}</small>{% endif %}
</label>
{% endif %}
<label>{{ nav.tr("td-fixed-decimal-access") }}
<select name="fixed_decimal_input" hx-post="/admin/tables/builder" hx-trigger="change"
hx-include="#table-form" hx-target="#builder" hx-swap="innerHTML"
hx-vals='{"action": "refresh"}'>
<option value="no" {% if !page.draft.columns.fixed_decimal_enabled() %}selected{% endif %}>{{ nav.tr("td-fixed-decimal-disabled") }}</option>
<option value="yes" {% if page.draft.columns.fixed_decimal_enabled() %}selected{% endif %}>{{ nav.tr("td-fixed-decimal-enabled") }}</option>
</select>
<small>{{ nav.tr("td-fixed-decimal-access-hint") }}</small>
</label>
<label>{{ nav.tr("td-column-type") }}
<select name="column_type_input" hx-post="/admin/tables/builder" hx-trigger="change"
hx-include="#table-form" hx-target="#builder" hx-swap="innerHTML"
hx-vals='{"action": "refresh"}'>
<option value="">{{ nav.tr("td-choose-type") }}</option>
{% for column_type in column_types %}
<option value="{{ column_type }}" {% if page.draft.columns.type_input == *column_type %}selected{% endif %}>{% if *column_type == "link" %}{{ nav.tr("td-fk-link") }}{% else %}{{ column_type }}{% endif %}</option>
<option value="{{ column_type }}" {% if page.draft.columns.type_input == *column_type %}selected{% endif %}>{% if *column_type == "link" %}{{ nav.tr("td-fk-link") }}{% else if *column_type == "numeric" %}{{ nav.tr("td-exact-decimal") }}{% else if *column_type == "decimal" %}{{ nav.tr("td-fixed-decimal-type") }}{% else %}{{ column_type }}{% endif %}</option>
{% endfor %}
</select>
</label>
@@ -161,6 +170,10 @@
{% endif %}
{% if page.draft.columns.show_decimal_arguments() %}
<div class="field-note col-span-full">
<span class="field-label">{{ nav.tr("td-fixed-decimal-warning-title") }}</span>
<p class="hint">{{ nav.tr("td-fixed-decimal-warning") }}</p>
</div>
<label>{{ nav.tr("td-precision") }}
<input name="decimal_precision_input" value="{{ page.draft.columns.decimal_precision_input }}"
inputmode="numeric" placeholder="12">

View File

@@ -29,6 +29,17 @@
<input name="column_name_input" value="{{ page.columns.name_input }}" placeholder="issued_on">
{% if page.columns.show_link_target() %}<small>{{ nav.tr("td-link-alias-hint") }}</small>{% endif %}
</label>
<label>{{ nav.tr("td-fixed-decimal-access") }}
<select name="fixed_decimal_input"
hx-post="/admin/tables/columns/add/builder{{ page.selection.query() }}"
hx-trigger="change" hx-include="#column-form"
hx-target="#column-panel" hx-swap="innerHTML"
hx-vals='{"action": "refresh"}'>
<option value="no" {% if !page.columns.fixed_decimal_enabled() %}selected{% endif %}>{{ nav.tr("td-fixed-decimal-disabled") }}</option>
<option value="yes" {% if page.columns.fixed_decimal_enabled() %}selected{% endif %}>{{ nav.tr("td-fixed-decimal-enabled") }}</option>
</select>
<small>{{ nav.tr("td-fixed-decimal-access-hint") }}</small>
</label>
<label>{{ nav.tr("td-column-type") }}
<select name="column_type_input"
hx-post="/admin/tables/columns/add/builder{{ page.selection.query() }}"
@@ -37,7 +48,7 @@
hx-vals='{"action": "refresh"}'>
<option value="">{{ nav.tr("td-choose-type") }}</option>
{% for column_type in column_types %}
<option value="{{ column_type }}" {% if page.columns.type_input == *column_type %}selected{% endif %}>{% if *column_type == "link" %}{{ nav.tr("td-fk-link") }}{% else %}{{ column_type }}{% endif %}</option>
<option value="{{ column_type }}" {% if page.columns.type_input == *column_type %}selected{% endif %}>{% if *column_type == "link" %}{{ nav.tr("td-fk-link") }}{% else if *column_type == "numeric" %}{{ nav.tr("td-exact-decimal") }}{% else if *column_type == "decimal" %}{{ nav.tr("td-fixed-decimal-type") }}{% else %}{{ column_type }}{% endif %}</option>
{% endfor %}
</select>
</label>
@@ -65,6 +76,10 @@
{% endif %}
{% if page.columns.show_decimal_arguments() %}
<div class="field-note col-span-full">
<span class="field-label">{{ nav.tr("td-fixed-decimal-warning-title") }}</span>
<p class="hint">{{ nav.tr("td-fixed-decimal-warning") }}</p>
</div>
<label>{{ nav.tr("td-precision") }}
<input name="decimal_precision_input" value="{{ page.columns.decimal_precision_input }}"
inputmode="numeric" placeholder="12">