aliasing3

This commit is contained in:
Priec
2026-08-13 14:41:27 +02:00
parent 5fe630fff7
commit 1f8b73793d
8 changed files with 110 additions and 62 deletions

2
client

Submodule client updated: c923d0de22...c15c62e88b

View File

@@ -108,18 +108,17 @@ message PostTableDefinitionRequest {
// column, though, so it is free to be anything: this is where that choice is // column, though, so it is free to be anything: this is where that choice is
// made, instead of a RenameColumnAlias call afterwards. // made, instead of a RenameColumnAlias call afterwards.
// //
// Only ACCOUNTING's columns may be renamed. Renaming a generated column // ACCOUNTING, PHONE and IBAN generated columns may be renamed: their
// requires its relationship to whatever generated it to be recorded, so the // relationships are recorded by physical column, so the rest of the system can
// rest of the system can find it without knowing its name; ACCOUNTING has that // find them without knowing their display names. ACCOUNTING_TRANSFER connectors
// in table_accounting_definitions, and nothing else does. The ACCOUNTING_ // are the exception and are refused here exactly as RenameColumnAlias refuses
// TRANSFER connectors and the PHONE and IBAN companions are refused, the // them.
// latter because a row write finds them by rebuilding their names from their
// parent column's name.
message GeneratedColumnAlias { message GeneratedColumnAlias {
// The name the backend would otherwise give the column: one of ACCOUNTING's // The name the backend would otherwise give the column: one of ACCOUNTING's
// "name", "tax_point_date", "debit", "credit" or "account". Must name a // "name", "tax_point_date", "debit", "credit" or "account", or a PHONE or
// column the request really generates -- an alias for anything else is // IBAN companion such as "work_phone_ext". Must name a column the request
// rejected rather than ignored, so a typo cannot pass silently. // really generates -- an alias for anything else is rejected rather than
// ignored, so a typo cannot pass silently.
string generated_name = 1; string generated_name = 1;
// What the column should be called instead. Same rules as any column name. // What the column should be called instead. Same rules as any column name.

Binary file not shown.

View File

@@ -56,20 +56,19 @@ pub struct PostTableDefinitionRequest {
/// column, though, so it is free to be anything: this is where that choice is /// column, though, so it is free to be anything: this is where that choice is
/// made, instead of a RenameColumnAlias call afterwards. /// made, instead of a RenameColumnAlias call afterwards.
/// ///
/// Only ACCOUNTING's columns may be renamed. Renaming a generated column /// ACCOUNTING, PHONE and IBAN generated columns may be renamed: their
/// requires its relationship to whatever generated it to be recorded, so the /// relationships are recorded by physical column, so the rest of the system can
/// rest of the system can find it without knowing its name; ACCOUNTING has that /// find them without knowing their display names. ACCOUNTING_TRANSFER connectors
/// in table_accounting_definitions, and nothing else does. The ACCOUNTING\_ /// are the exception and are refused here exactly as RenameColumnAlias refuses
/// TRANSFER connectors and the PHONE and IBAN companions are refused, the /// them.
/// latter because a row write finds them by rebuilding their names from their
/// parent column's name.
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GeneratedColumnAlias { pub struct GeneratedColumnAlias {
/// The name the backend would otherwise give the column: one of ACCOUNTING's /// The name the backend would otherwise give the column: one of ACCOUNTING's
/// "name", "tax_point_date", "debit", "credit" or "account". Must name a /// "name", "tax_point_date", "debit", "credit" or "account", or a PHONE or
/// column the request really generates -- an alias for anything else is /// IBAN companion such as "work_phone_ext". Must name a column the request
/// rejected rather than ignored, so a typo cannot pass silently. /// really generates -- an alias for anything else is rejected rather than
/// ignored, so a typo cannot pass silently.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub generated_name: ::prost::alloc::string::String, pub generated_name: ::prost::alloc::string::String,
/// What the column should be called instead. Same rules as any column name. /// What the column should be called instead. Same rules as any column name.

2
server

Submodule server updated: 9984e3b272...6e8ac32389

View File

@@ -21,6 +21,9 @@ use crate::{
/// them, and [`TableDraft::preview_rows`] is where that is explained. /// them, and [`TableDraft::preview_rows`] is where that is explained.
pub(crate) const ACCOUNTING_FIELD_TYPE: &str = "accounting"; pub(crate) const ACCOUNTING_FIELD_TYPE: &str = "accounting";
/// The compound type whose connectors remain tied to their backend names.
pub(crate) const ACCOUNTING_TRANSFER_FIELD_TYPE: &str = "accounting_transfer";
/// The virtual field the ACCOUNTING foreign key is written as. It is a column /// The virtual field the ACCOUNTING foreign key is written as. It is a column
/// like any other once created, so it can be aliased too. /// like any other once created, so it can be aliased too.
pub(crate) const ACCOUNT_API_COLUMN: &str = "account"; pub(crate) const ACCOUNT_API_COLUMN: &str = "account";
@@ -225,18 +228,13 @@ impl TableDraft {
/// The generated columns this draft would let the user rename, in the order /// The generated columns this draft would let the user rename, in the order
/// they appear in the column list. /// they appear in the column list.
/// ///
/// ACCOUNTING's, and only ACCOUNTING's. A generated column can carry a name /// Every generated column whose relationship is recorded independently of
/// of its own once the rest of the system can find it without that name -- /// its display name. ACCOUNTING_TRANSFER's connectors are the exception:
/// which for ACCOUNTING is `table_accounting_definitions`, recording its /// the backend still resolves those by their fixed names.
/// columns by physical name. The ACCOUNTING_TRANSFER connectors and the
/// PHONE and IBAN companions have no such record: a companion is found on
/// write by rebuilding its name from its parent's, so renaming one would
/// hide it from the write path. The backend refuses those, and this does
/// not offer what the backend refuses.
pub(crate) fn aliasable_generated_columns(&self) -> Vec<String> { pub(crate) fn aliasable_generated_columns(&self) -> Vec<String> {
let mut names = Vec::new(); let mut names = Vec::new();
for (index, column) in self.columns.added.iter().enumerate() { for (index, column) in self.columns.added.iter().enumerate() {
if column.data_type != ACCOUNTING_FIELD_TYPE { if column.data_type == ACCOUNTING_TRANSFER_FIELD_TYPE {
continue; continue;
} }
names.extend( names.extend(
@@ -245,7 +243,9 @@ impl TableDraft {
.iter() .iter()
.map(|generated| generated.name.clone()), .map(|generated| generated.name.clone()),
); );
names.push(ACCOUNT_API_COLUMN.to_string()); if column.data_type == ACCOUNTING_FIELD_TYPE {
names.push(ACCOUNT_API_COLUMN.to_string());
}
} }
names names
} }
@@ -555,24 +555,34 @@ mod tests {
); );
} }
/// Only ACCOUNTING's columns are offered. The transfer connectors are /// ACCOUNTING, PHONE and IBAN companions may be aliased. Transfer
/// resolved by name by the posting engine, and a phone or IBAN companion is /// connectors remain fixed because the posting engine resolves their names.
/// found on write by rebuilding its name from its parent's -- the backend
/// refuses both, so neither is offered here.
#[test] #[test]
fn only_accounting_columns_are_aliasable() { fn every_recorded_generated_column_is_aliasable() {
for (name, data_type) in [ let phone = draft_with_column("work_phone", "phone");
("accounting_transfer", "accounting_transfer"), assert_eq!(
("work_phone", "phone"), phone.aliasable_generated_columns(),
("bank_account", "iban"), [
] { "work_phone_ext",
let draft = draft_with_column(name, data_type); "work_phone_type",
"work_phone_country",
"work_phone_calling_code",
]
);
assert!( let iban = draft_with_column("bank_account", "iban");
draft.aliasable_generated_columns().is_empty(), assert_eq!(
"`{data_type}` should offer no aliases" iban.aliasable_generated_columns(),
); [
} "bank_account_country",
"bank_account_bban",
"bank_account_bank_identifier",
"bank_account_branch_identifier",
]
);
let transfer = draft_with_column("accounting_transfer", "accounting_transfer");
assert!(transfer.aliasable_generated_columns().is_empty());
} }
#[test] #[test]

View File

@@ -13,7 +13,10 @@
use crate::schema::{ColumnCatalog, ColumnDraft, columns_from_rows}; use crate::schema::{ColumnCatalog, ColumnDraft, columns_from_rows};
use super::draft::{ACCOUNT_API_COLUMN, ACCOUNTING_FIELD_TYPE, GeneratedAlias, 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". /// The `profile_name` option meaning "create a new profile too".
pub(crate) const NEW_PROFILE: &str = "__new__"; pub(crate) const NEW_PROFILE: &str = "__new__";
@@ -223,12 +226,10 @@ impl AddTablePageState {
alias: String::new(), alias: String::new(),
}); });
// A generated column is named by the backend, but the name is a // Every generated column with persisted provenance may be named by
// display name, and ACCOUNTING's columns can be given one of the // the request. Transfer connectors are still resolved by fixed
// user's own in the request that creates them. Only ACCOUNTING's: // backend names and remain the exception.
// see `TableDraft::aliasable_generated_columns` for why the other let aliasable = column.data_type != ACCOUNTING_TRANSFER_FIELD_TYPE;
// generated columns cannot be renamed at all.
let aliasable = column.data_type == ACCOUNTING_FIELD_TYPE;
for generated in columns.generated_columns_of(index) { for generated in columns.generated_columns_of(index) {
let mut tags = vec![format!("generated by {}", column.data_type)]; let mut tags = vec![format!("generated by {}", column.data_type)];

View File

@@ -664,11 +664,22 @@ impl ColumnDraft {
/// compound one. This is what the column list shows underneath it, so the /// compound one. This is what the column list shows underneath it, so the
/// columns a definition row brings are visible while the table is still /// columns a definition row brings are visible while the table is still
/// being described. /// being described.
pub(crate) fn generated_columns_of(&self, index: usize) -> &[GeneratedColumn] { pub(crate) fn generated_columns_of(&self, index: usize) -> Vec<GeneratedColumn> {
self.added let Some(column) = self.added.get(index) else {
.get(index) return Vec::new();
.map(|column| self.catalog.generated_columns(&column.data_type)) };
.unwrap_or_default() let generated = self.catalog.generated_columns(&column.data_type);
let default_prefix = format!("{}_", column.data_type);
generated
.iter()
.cloned()
.map(|mut companion| {
if let Some(suffix) = companion.name.strip_prefix(&default_prefix) {
companion.name = format!("{}_{}", column.name, suffix);
}
companion
})
.collect()
} }
/// Whether a column can be indexed or identify a row. A compound column /// Whether a column can be indexed or identify a row. A compound column
@@ -1115,7 +1126,23 @@ pub(crate) mod tests {
grouped("gtin_12", "gtin"), grouped("gtin_12", "gtin"),
grouped("gtin_13", "gtin"), grouped("gtin_13", "gtin"),
grouped("gtin_14", "gtin"), grouped("gtin_14", "gtin"),
declarable("iban"), ColumnType {
generated_columns: vec![
generated_column("iban_country", "iban_country", false),
generated_column("iban_bban", "iban_bban", false),
generated_column(
"iban_bank_identifier",
"iban_bank_identifier",
false,
),
generated_column(
"iban_branch_identifier",
"iban_branch_identifier",
false,
),
],
..declarable("iban")
},
ColumnType { ColumnType {
declarable: false, declarable: false,
..declarable("iban_bban") ..declarable("iban_bban")
@@ -1135,7 +1162,19 @@ pub(crate) mod tests {
..declarable("numeric") ..declarable("numeric")
}, },
declarable("period"), declarable("period"),
declarable("phone"), ColumnType {
generated_columns: vec![
generated_column("phone_ext", "phone_extension", false),
generated_column("phone_type", "phone_type", false),
generated_column("phone_country", "phone_country", false),
generated_column(
"phone_calling_code",
"phone_calling_code",
false,
),
],
..declarable("phone")
},
ColumnType { ColumnType {
declarable: false, declarable: false,
sql_type: "INTEGER".to_string(), sql_type: "INTEGER".to_string(),