From 0d7855642882d0855b1b115e3d66907374b7a192 Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 7 Aug 2026 22:39:59 +0200 Subject: [PATCH] multirow FK --- common/build.rs | 4 - common/proto/table_definition.proto | 26 ++-- common/src/proto/descriptor.bin | Bin 149680 -> 149254 bytes common/src/proto/komp_ac.table_definition.rs | 29 ++-- server | 2 +- web/src/pages/add_table/draft.rs | 148 ++----------------- web/src/pages/add_table/loader.rs | 2 +- web/src/pages/add_table/logic.rs | 1 - web/src/pages/add_table/state.rs | 28 +--- web/templates/pages/add_table/builder.html | 22 +-- 10 files changed, 46 insertions(+), 216 deletions(-) diff --git a/common/build.rs b/common/build.rs index a52fc88d..efb54993 100644 --- a/common/build.rs +++ b/common/build.rs @@ -157,10 +157,6 @@ fn main() -> Result<(), Box> { ".komp_ac.table_definition.ColumnDefinition", "#[derive(serde::Serialize, serde::Deserialize)]", ) - .type_attribute( - ".komp_ac.table_definition.TableLink", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) .type_attribute( ".komp_ac.table_definition.PostTableDefinitionRequest", "#[derive(serde::Serialize, serde::Deserialize)]", diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 575cf6ed..6ed207e1 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -48,19 +48,6 @@ service TableDefinition { rpc DeleteTable(DeleteTableRequest) returns (DeleteTableResponse); } -// A single link to another table within the same profile (schema). -message TableLink { - // Name of an existing table within the same profile to link to. - // For each link, a "_id" column is created on the new table. - // That column references ""(id) and adds an index automatically. - string linked_table_name = 1; - - // If true, the generated foreign key column is NOT NULL. - // Otherwise the column allows NULL. - // Duplicate links to the same target table in one request are rejected. - bool required = 2; -} - // Defines the input for creating a new table definition. message PostTableDefinitionRequest { // Table name to create inside the target profile. @@ -69,10 +56,6 @@ message PostTableDefinitionRequest { // Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "_id". string table_name = 1; - // List of links (foreign keys) to existing tables in the same profile. - // Each will automatically get a "_id" column and an index. - repeated TableLink links = 2; - // List of user-defined columns (adds to system/id/fk columns). repeated ColumnDefinition columns = 3; @@ -187,6 +170,9 @@ message ColumnDefinition { // DURATION // PERIOD // DECIMAL(p,s) → NUMERIC(p,s) + // LINK(table) → BIGINT referencing that table in the same profile, indexed + // automatically. A table may hold several links to the same + // target as long as the columns are named differently. // DECIMAL args must be integers (no sign, no dot, no leading zeros); // s ≤ p and p ≥ 1. string field_type = 2; @@ -371,6 +357,12 @@ enum ColumnTypeSpelling { // at least 1 and scale no greater than precision; neither may carry a sign, // a decimal point, or leading zeros. COLUMN_TYPE_SPELLING_DECIMAL = 1; + + // The name takes the name of another table in the same profile: + // "link(adresar)". The column holds that table's id, and the server creates + // the foreign key and its index. A picker offers the profile's other tables + // as the argument. + COLUMN_TYPE_SPELLING_LINK = 2; } // Response describing the whole column-type vocabulary. diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 33e504fd88e74104aa6801306dfcfab21480134c..7437890dded7e86e0250a5297568ae53ec6806f7 100644 GIT binary patch delta 7251 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 delta 7647 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 B(\_id" column is created on the new table. - /// That column references ""(id) and adds an index automatically. - #[prost(string, tag = "1")] - pub linked_table_name: ::prost::alloc::string::String, - /// If true, the generated foreign key column is NOT NULL. - /// Otherwise the column allows NULL. - /// Duplicate links to the same target table in one request are rejected. - #[prost(bool, tag = "2")] - pub required: bool, -} /// Defines the input for creating a new table definition. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -24,10 +9,6 @@ pub struct PostTableDefinitionRequest { /// Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "\_id". #[prost(string, tag = "1")] pub table_name: ::prost::alloc::string::String, - /// List of links (foreign keys) to existing tables in the same profile. - /// Each will automatically get a "\_id" column and an index. - #[prost(message, repeated, tag = "2")] - pub links: ::prost::alloc::vec::Vec, /// List of user-defined columns (adds to system/id/fk columns). #[prost(message, repeated, tag = "3")] pub columns: ::prost::alloc::vec::Vec, @@ -153,6 +134,9 @@ pub struct ColumnDefinition { /// DURATION /// PERIOD /// DECIMAL(p,s) → NUMERIC(p,s) + /// LINK(table) → BIGINT referencing that table in the same profile, indexed + /// automatically. A table may hold several links to the same + /// target as long as the columns are named differently. /// DECIMAL args must be integers (no sign, no dot, no leading zeros); /// s ≤ p and p ≥ 1. #[prost(string, tag = "2")] @@ -486,6 +470,11 @@ pub enum ColumnTypeSpelling { /// at least 1 and scale no greater than precision; neither may carry a sign, /// a decimal point, or leading zeros. Decimal = 1, + /// The name takes the name of another table in the same profile: + /// "link(adresar)". The column holds that table's id, and the server creates + /// the foreign key and its index. A picker offers the profile's other tables + /// as the argument. + Link = 2, } impl ColumnTypeSpelling { /// String value of the enum field names used in the ProtoBuf definition. @@ -496,6 +485,7 @@ impl ColumnTypeSpelling { match self { Self::Bare => "COLUMN_TYPE_SPELLING_BARE", Self::Decimal => "COLUMN_TYPE_SPELLING_DECIMAL", + Self::Link => "COLUMN_TYPE_SPELLING_LINK", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -503,6 +493,7 @@ impl ColumnTypeSpelling { match value { "COLUMN_TYPE_SPELLING_BARE" => Some(Self::Bare), "COLUMN_TYPE_SPELLING_DECIMAL" => Some(Self::Decimal), + "COLUMN_TYPE_SPELLING_LINK" => Some(Self::Link), _ => None, } } diff --git a/server b/server index 8ed66653..9b5956ce 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 8ed666531ba2d5745f8b3c0019333ce1b7f7785c +Subproject commit 9b5956ce99709ddad209c2b870ddcc19cb6580a6 diff --git a/web/src/pages/add_table/draft.rs b/web/src/pages/add_table/draft.rs index 5869a022..863ffc52 100644 --- a/web/src/pages/add_table/draft.rs +++ b/web/src/pages/add_table/draft.rs @@ -8,65 +8,16 @@ //! //! What a *column* may be is not here: that is [`crate::schema`], which the //! append screen in `admin/table_definition` shares. This module is only what -//! is true of a table being created — its profile, its name, its links, and +//! is true of a table being created — its profile, its name, its columns, and //! what identifies one of its rows. use crate::{ definitions::table_definition::{ - PostTableDefinitionRequest, TableLink as ProtoTableLink, + PostTableDefinitionRequest, }, schema::{ColumnCatalog, ColumnDraft, proto_columns, validate_identifier}, }; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub(crate) enum LinkMode { - #[default] - None, - Optional, - Required, -} - -impl LinkMode { - pub(crate) fn label(self) -> &'static str { - match self { - Self::None => "none", - Self::Optional => "optional", - Self::Required => "required", - } - } - - /// Cycles none → optional → required → none, as `Select` does in the TUI. - pub(crate) fn next(self) -> Self { - match self { - Self::None => Self::Optional, - Self::Optional => Self::Required, - Self::Required => Self::None, - } - } - - pub(crate) fn from_label(value: &str) -> Self { - match value.trim() { - "optional" => Self::Optional, - "required" => Self::Required, - _ => Self::None, - } - } - - pub(crate) fn is_active(self) -> bool { - !matches!(self, Self::None) - } - - pub(crate) fn is_required(self) -> bool { - matches!(self, Self::Required) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct LinkDefinition { - pub linked_table_name: String, - pub mode: LinkMode, -} - /// One row of the "Table definition preview" — the schema as it will exist. pub(crate) struct PreviewRow { pub mark: String, @@ -91,7 +42,9 @@ pub(crate) struct TableDraft { /// The column panel: the pending column and the ones already described. pub columns: ColumnDraft, - pub links: Vec, + /// Tables in the target profile, offered as `link(...)` targets by the + /// column picker. + pub relation_tables: Vec, /// Columns identifying a row to users, in the order they are shown. /// Empty means rows are identified by their id alone. pub row_display_columns: Vec, @@ -133,12 +86,6 @@ impl TableDraft { Ok(format!("Column `{}` removed.", removed.name)) } - pub(crate) fn cycle_link_mode(&mut self, index: usize) { - if let Some(link) = self.links.get_mut(index) { - link.mode = link.mode.next(); - } - } - /// Adds or removes one display-column candidate. /// /// Index 0 is `id`, which is not a display column of its own: choosing it @@ -175,25 +122,12 @@ impl TableDraft { } } - /// Rebuilds the link list from the tables available in the target profile, - /// keeping whatever mode each surviving link already had. + /// Records the tables the target profile offers as link targets. A table + /// cannot link to itself, so its own name is never among them. pub(crate) fn set_available_relation_tables(&mut self, table_names: Vec) { - let previous_modes = self - .links - .iter() - .map(|link| (link.linked_table_name.clone(), link.mode)) - .collect::>(); - - self.links = table_names + self.relation_tables = table_names .into_iter() .filter(|table_name| table_name != &self.table_name) - .map(|linked_table_name| LinkDefinition { - mode: previous_modes - .get(&linked_table_name) - .copied() - .unwrap_or(LinkMode::None), - linked_table_name, - }) .collect(); } @@ -249,20 +183,6 @@ impl TableDraft { }, ]; - for link in self.links.iter().filter(|link| link.mode.is_active()) { - rows.push(PreviewRow { - mark: String::new(), - column: format!("{}_id", link.linked_table_name), - data_type: "BIGINT".to_string(), - option: if link.mode.is_required() { - "required".to_string() - } else { - "optional".to_string() - }, - source: "relation".to_string(), - }); - } - for column in &self.columns.added { rows.push(PreviewRow { mark: self @@ -324,16 +244,6 @@ impl TableDraft { profile_name: self.effective_profile_name(), columns: proto_columns(&self.columns.added), indexes: self.columns.selected_index_names(), - links: self - .links - .iter() - .filter(|link| link.mode.is_active()) - .map(|link| ProtoTableLink { - linked_table_name: link.linked_table_name.clone(), - required: link.mode.is_required(), - name: link.linked_table_name.clone(), - }) - .collect(), accounting_currency: if self.creating_new_profile { self.accounting_currency.trim().to_ascii_uppercase() } else { @@ -430,34 +340,12 @@ mod tests { } #[test] - fn links_keep_their_mode_when_the_table_list_is_reloaded() { - let mut draft = draft_with_column("total", "int"); - draft.set_available_relation_tables(vec!["customer".into(), "project".into()]); - draft.cycle_link_mode(0); // none -> optional - draft.cycle_link_mode(0); // optional -> required - - draft.set_available_relation_tables(vec![ - "customer".into(), - "project".into(), - "address".into(), - ]); - - assert_eq!(draft.links[0].mode, LinkMode::Required); - assert_eq!(draft.links[2].mode, LinkMode::None); - - let request = draft.into_request().unwrap(); - assert_eq!(request.links.len(), 1); - assert_eq!(request.links[0].linked_table_name, "customer"); - assert!(request.links[0].required); - } - - #[test] - fn a_table_never_links_to_itself() { - let mut draft = draft_with_column("total", "int"); + fn a_table_never_offers_itself_as_a_link_target() { + let mut draft = TableDraft::new(); + draft.table_name = "invoice".into(); draft.set_available_relation_tables(vec!["invoice".into(), "customer".into()]); - assert_eq!(draft.links.len(), 1); - assert_eq!(draft.links[0].linked_table_name, "customer"); + assert_eq!(draft.relation_tables, vec!["customer".to_string()]); } #[test] @@ -513,10 +401,8 @@ mod tests { } #[test] - fn the_preview_shows_system_relation_and_user_columns() { - let mut draft = draft_with_column("number", "text"); - draft.set_available_relation_tables(vec!["customer".into()]); - draft.cycle_link_mode(0); + fn the_preview_shows_system_and_user_columns() { + let draft = draft_with_column("number", "text"); let rows = draft.preview_rows(); let columns = rows @@ -524,11 +410,7 @@ mod tests { .map(|row| row.column.as_str()) .collect::>(); - assert_eq!( - columns, - vec!["id", "deleted", "customer_id", "number", "created_at"] - ); - assert_eq!(rows[2].option, "optional"); + assert_eq!(columns, vec!["id", "deleted", "number", "created_at"]); // No display column chosen, so `id` identifies the row. assert_eq!(rows[0].mark, "[x]"); } diff --git a/web/src/pages/add_table/loader.rs b/web/src/pages/add_table/loader.rs index aeed21b6..34403b78 100644 --- a/web/src/pages/add_table/loader.rs +++ b/web/src/pages/add_table/loader.rs @@ -80,7 +80,7 @@ pub(crate) async fn load_page( // A brand-new (or not-yet-named) profile has nothing to link to. None => { draft.existing_profile_tables.clear(); - draft.links.clear(); + draft.relation_tables.clear(); } } diff --git a/web/src/pages/add_table/logic.rs b/web/src/pages/add_table/logic.rs index 19a5957c..36867c4c 100644 --- a/web/src/pages/add_table/logic.rs +++ b/web/src/pages/add_table/logic.rs @@ -142,7 +142,6 @@ fn apply_action(page: &mut AddTablePageState, form: &BuilderForm) { Err(message) => page.error = Some(message), }, "toggle-index" => page.draft.columns.toggle_indexed(index), - "cycle-link" => page.draft.cycle_link_mode(index), "toggle-display" => page.draft.toggle_row_display_candidate(index), _ => {} } diff --git a/web/src/pages/add_table/state.rs b/web/src/pages/add_table/state.rs index 99a43692..7e5d1aa6 100644 --- a/web/src/pages/add_table/state.rs +++ b/web/src/pages/add_table/state.rs @@ -1,7 +1,7 @@ //! Wire format for the builder form, and the page state the templates read. //! //! HTTP is stateless, so the whole draft travels with every interaction: each -//! already-added column, link and display column is posted back as a set of +//! already-added column and display column is posted back as a set of //! parallel repeated fields. `serde_html_form` (via `axum_extra::extract::Form`) //! decodes the repeats into `Vec`s, which `to_draft` zips back into a //! [`TableDraft`]. @@ -13,7 +13,7 @@ use crate::schema::{ColumnCatalog, ColumnDraft, columns_from_rows}; -use super::draft::{LinkDefinition, LinkMode, TableDraft}; +use super::draft::TableDraft; /// The `profile_name` option meaning "create a new profile too". pub(crate) const NEW_PROFILE: &str = "__new__"; @@ -72,11 +72,9 @@ pub(crate) struct BuilderForm { #[serde(default)] pub column_currencies: Vec, - // One entry per link target offered by the profile, in order. + // Tables the profile offers as link targets, in order. #[serde(default)] - pub link_tables: Vec, - #[serde(default)] - pub link_modes: Vec, + pub relation_tables: Vec, #[serde(default)] pub row_display_columns: Vec, @@ -118,14 +116,6 @@ impl BuilderForm { creating_table: true, }; - let link_count = self.link_tables.len().min(self.link_modes.len()); - let links = (0..link_count) - .map(|index| LinkDefinition { - linked_table_name: self.link_tables[index].clone(), - mode: LinkMode::from_label(&self.link_modes[index]), - }) - .collect(); - // Drop display columns whose column is gone, so a stale post cannot // send a display column that no longer exists. let row_display_columns = self @@ -146,7 +136,7 @@ impl BuilderForm { accounting_currency: self.accounting_currency.clone(), table_name: self.table_name.clone(), columns, - links, + relation_tables: self.relation_tables.clone(), row_display_columns, // Filled in by the loader from the live profile tree, never by the // client: it is what duplicate table names are checked against. @@ -240,22 +230,20 @@ mod tests { column_quantity_ledger: vec!["no".into(), "no".into()], column_rounding: vec!["exact".into(), "half-up".into()], column_currencies: vec![String::new(), "EUR".into()], - link_tables: vec!["customer".into(), "project".into()], - link_modes: vec!["required".into(), "none".into()], + relation_tables: vec!["customer".into(), "project".into()], row_display_columns: vec!["number".into()], ..Default::default() } } #[test] - fn round_trips_columns_links_and_display_columns() { + fn round_trips_columns_relation_tables_and_display_columns() { let draft = posted_form().to_draft(); assert_eq!(draft.columns.added.len(), 2); assert!(draft.columns.added[0].indexed); assert_eq!(draft.columns.added[1].money_mode, MoneyMode::Rounded); - assert_eq!(draft.links[0].mode, LinkMode::Required); - assert_eq!(draft.links[1].mode, LinkMode::None); + assert_eq!(draft.relation_tables, vec!["customer", "project"]); assert_eq!(draft.row_display_columns, vec!["number"]); assert!(!draft.creating_new_profile); } diff --git a/web/templates/pages/add_table/builder.html b/web/templates/pages/add_table/builder.html index 3fe6e298..235b3ee8 100644 --- a/web/templates/pages/add_table/builder.html +++ b/web/templates/pages/add_table/builder.html @@ -195,26 +195,8 @@
-

Relations

- {% if page.draft.links.is_empty() %} -

No other tables in this profile to link to.

- {% else %} -

Each active relation adds a <table>_id column.

-
    - {% for link in page.draft.links %} -
  • - - {{ link.linked_table_name }} -
  • - {% endfor %} -
- {% endif %} - - {% for link in page.draft.links %} - - + {% for table in page.draft.relation_tables %} + {% endfor %}