aliasing for accounting, where accounting real column names are the same as others, which is 0..n as opposed to previously real names and hardcoded search via those. Now we are using a look up table for the numbers mapping to look em up

This commit is contained in:
Priec
2026-08-13 13:41:07 +02:00
parent 774c9a3ce0
commit 27d3e9af34
11 changed files with 444 additions and 10 deletions

View File

@@ -13,7 +13,10 @@
use crate::schema::{ColumnCatalog, ColumnDraft, columns_from_rows};
use super::draft::{ACCOUNTING_FIELD_TYPE, TableDraft};
use super::draft::{
ACCOUNT_API_COLUMN, ACCOUNTING_FIELD_TYPE, ACCOUNTING_TRANSFER_FIELD_TYPE, GeneratedAlias,
TableDraft,
};
/// The `profile_name` option meaning "create a new profile too".
pub(crate) const NEW_PROFILE: &str = "__new__";
@@ -82,6 +85,13 @@ pub(crate) struct BuilderForm {
#[serde(default)]
pub row_display_columns: Vec<String>,
// One pair per generated column offered an alias, in list order: the
// generated column's own name, and what the user typed for it.
#[serde(default)]
pub generated_alias_sources: Vec<String>,
#[serde(default)]
pub generated_alias_names: Vec<String>,
}
impl BuilderForm {
@@ -148,6 +158,18 @@ impl BuilderForm {
// Filled in by the loader from the live profile tree, never by the
// client: it is what duplicate table names are checked against.
existing_profile_tables: Vec::new(),
// Which of these still name a generated column is the catalog's
// answer, and the catalog is not filled in yet, so a pair is kept
// here and matched up when the aliases are read.
generated_aliases: self
.generated_alias_sources
.iter()
.zip(self.generated_alias_names.iter())
.map(|(source, alias)| GeneratedAlias {
source: source.trim().to_string(),
alias: alias.trim().to_string(),
})
.collect(),
}
}
}
@@ -200,8 +222,16 @@ impl AddTablePageState {
indexable: columns.is_indexable(index),
indexed: column.indexed,
tags,
alias_source: None,
alias: String::new(),
});
// A generated column is named by the backend, but the name is a
// display name: it can be aliased, and the alias is applied as a
// rename the moment the table exists. The connectors of an
// ACCOUNTING_TRANSFER are the exception the backend protects.
let aliasable = column.data_type != ACCOUNTING_TRANSFER_FIELD_TYPE;
for generated in columns.generated_columns_of(index) {
let mut tags = vec![format!("generated by {}", column.data_type)];
if generated.inherits_currency {
@@ -216,6 +246,8 @@ impl AddTablePageState {
indexable: false,
indexed: false,
tags,
alias_source: aliasable.then(|| generated.name.clone()),
alias: self.draft.alias_for(&generated.name).to_string(),
});
}
@@ -233,8 +265,13 @@ impl AddTablePageState {
indexed: true,
tags: vec![
"system column".to_string(),
"written as account".to_string(),
format!(
"written as {}",
self.draft.generated_display_name(ACCOUNT_API_COLUMN)
),
],
alias_source: Some(ACCOUNT_API_COLUMN.to_string()),
alias: self.draft.alias_for(ACCOUNT_API_COLUMN).to_string(),
});
}
}
@@ -287,6 +324,12 @@ pub(crate) struct ColumnRow {
pub indexable: bool,
pub indexed: bool,
pub tags: Vec<String>,
/// The generated column an alias would rename, when this row is one that
/// may be aliased. `None` for a declared column — it is named where it is
/// described — and for the columns the backend refuses to rename.
pub alias_source: Option<String>,
/// The alias typed for it so far.
pub alias: String,
}
pub(crate) struct RowDisplayCandidate {
@@ -425,6 +468,51 @@ mod tests {
assert!(row("name").tags.contains(&"generated by accounting".to_string()));
}
/// The generated columns of an ACCOUNTING row are the user's to alias, and
/// the alias travels back with the rest of the draft.
#[test]
fn the_generated_columns_of_an_accounting_row_offer_an_alias() {
let mut form = posted_form();
form.column_names.push("accounting".into());
form.column_types.push("accounting".into());
form.column_indexed.push("no".into());
form.column_quantity_ledger.push("no".into());
form.column_rounding.push("exact".into());
form.column_currencies.push("EUR".into());
form.generated_alias_sources = vec!["debit".into(), "account".into()];
form.generated_alias_names = vec![" md ".into(), "ucet".into()];
let mut draft = form.to_draft();
draft.columns.catalog = crate::schema::tests::catalog();
assert_eq!(draft.alias_for("debit"), "md");
assert_eq!(
draft.aliased_generated_columns(),
vec![
("debit".to_string(), "md".to_string()),
("account".to_string(), "ucet".to_string()),
]
);
let page = AddTablePageState {
nav: crate::ui::Nav::default(),
profiles: Vec::new(),
draft,
status: None,
error: None,
};
let rows = page.column_rows();
let row = |name: &str| rows.iter().find(|row| row.name == name).unwrap();
assert_eq!(row("debit").alias_source.as_deref(), Some("debit"));
assert_eq!(row("debit").alias, "md");
assert_eq!(row("account_id").alias_source.as_deref(), Some("account"));
assert!(row("account_id").tags.contains(&"written as ucet".to_string()));
assert!(
row("accounting").alias_source.is_none(),
"a declared column is named where it is described"
);
}
#[test]
fn the_draft_never_trusts_the_posted_table_list() {
// `existing_profile_tables` is what duplicate-name checks read, so it