From 751bfd9226925b4723d3230410d339f1dff04dcf Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 8 Aug 2026 12:40:42 +0200 Subject: [PATCH] multiFK2 --- client | 2 +- common/proto/table_definition.proto | 24 ++++++++----- common/proto/table_script.proto | 10 ++++++ common/proto/table_structure.proto | 2 +- common/proto/tables_data.proto | 6 ++-- common/src/proto/descriptor.bin | Bin 149847 -> 150800 bytes common/src/proto/komp_ac.table_definition.rs | 24 ++++++++----- common/src/proto/komp_ac.table_script.rs | 12 +++++++ common/src/proto/komp_ac.table_structure.rs | 2 +- common/src/proto/komp_ac.tables_data.rs | 8 ++--- server | 2 +- web/src/schema/mod.rs | 35 +++++++++++++++---- 12 files changed, 91 insertions(+), 36 deletions(-) diff --git a/client b/client index 8538beba..da1557a2 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 8538beba2a7ec27f4205131fd11f84df653082b6 +Subproject commit da1557a2d4a08be07f7c3af93529aa240a060744 diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 59c2f5c7..4ce00d78 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -53,16 +53,18 @@ message PostTableDefinitionRequest { // Table name to create inside the target profile. // Must be lowercase, alphanumeric with underscores, // start with a letter, and be <= 63 chars. - // Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "_id". + // Forbidden names: "id", "deleted", "created_at", "row_revision" -- the + // system columns, which share one namespace with table names wherever the + // two are named side by side. The "_id" suffix is allowed. string table_name = 1; - // List of user-defined columns (adds to system/id/fk columns). + // The table's columns, including its links. System columns are added on top. repeated ColumnDefinition columns = 3; - // List of column names to be indexed (must match existing user-defined columns). - // Indexes can target only user-defined columns; system columns ("id", "deleted", - // "created_at", "row_revision") and automatically generated foreign key ("*_id") columns already - // have indexes. Requests trying to index those columns are rejected. + // Column names to index, matching names declared above. System columns + // ("id", "deleted", "created_at", "row_revision") already have indexes, and + // a LINK column is indexed when it is created. Requests naming either are + // rejected. repeated string indexes = 4; // Name of profile (Postgres schema) where the table will be created. @@ -142,9 +144,13 @@ enum MoneyRounding { // Describes one user-defined column for a table. message ColumnDefinition { - // Column name that follows the same validation rules as table_name. - // Must be lowercase, start with a letter, no uppercase characters, - // and cannot be "id", "deleted", "created_at", "row_revision", or end with "_id". + // Must be lowercase, start with a letter, and use only lowercase letters, + // digits and underscores. + // + // Cannot be "id", "deleted", "created_at" or "row_revision": those are + // system columns, and a data request names system and user columns in one + // namespace. Any other name is free, including one ending in "_id" -- no + // column name is derived from a table name, so nothing collides. string name = 1; // Logical column type. Supported values (case-insensitive): diff --git a/common/proto/table_script.proto b/common/proto/table_script.proto index a5ae6163..94fb3f5c 100644 --- a/common/proto/table_script.proto +++ b/common/proto/table_script.proto @@ -181,6 +181,16 @@ message ScriptDependency { string operation = 4; // Relationship table used to match the owner row to the related collection. string via_table = 5; + // Column of the current row holding the referenced row's id: the link this + // dependency travels, resolved when the script was saved. Empty when no link + // is travelled -- a self-reference, or an aggregate, which follows a path + // instead. A table may link to one target several times, so this is the only + // thing that says which link was meant; never derive it from target_table. + string link_column = 6; + // The first argument exactly as the script wrote it: a link alias, or the + // target table's name where one link makes that unambiguous. The Steel + // runtime looks its inputs up by this text. + string name_in_script = 7; } // Identifies the active form row whose external Steel inputs must be hydrated. diff --git a/common/proto/table_structure.proto b/common/proto/table_structure.proto index d840f2f2..70ea77b7 100644 --- a/common/proto/table_structure.proto +++ b/common/proto/table_structure.proto @@ -46,7 +46,7 @@ message GetTableStructureResponse { message TableStructureResponse { // Columns of the physical table, including system columns (id, deleted, // created_at, row_revision), user-defined columns, and any foreign-key columns such as - // "_id", plus the dedicated "account_id" column on + // named by whoever declared each link, plus the dedicated "account_id" on // ACCOUNTING-enabled tables. May be empty if the physical table is missing. repeated TableColumn columns = 1; } diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index 5fa57a7a..2d95c3fe 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -62,7 +62,7 @@ service TablesData { // - Validates profile and table definition // - Returns all columns as strings (COALESCE(col::TEXT, '') AS col) // including: id, deleted, row_revision, all user-defined columns, and FK columns - // named "_id" for each table link, plus "account_id" on + // named by whoever declared each link, plus "account_id" on // ACCOUNTING-enabled tables // - Fails with NOT_FOUND if record does not exist or is soft-deleted // - If the physical table is missing but the definition exists, returns INTERNAL @@ -101,7 +101,7 @@ message PostTableDataRequest { // - User-defined columns from the table definition // - System/FK columns: // • "deleted" (BOOLEAN), optional; default FALSE if not provided - // • "_id" (BIGINT) for each table link + // • one BIGINT per link, named by whoever declared it // • "account_id" (BIGINT) on ACCOUNTING-enabled tables // // Type expectations by SQL type: @@ -250,7 +250,7 @@ message GetTableDataResponse { // Map of column_name → stringified value for: // - id, deleted // - all user-defined columns from the table definition - // - FK columns named "_id" for each table link + // - one column per link, named by whoever declared it // - account_id for ACCOUNTING-enabled tables // // All values are returned as TEXT via col::TEXT and COALESCEed to empty string diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 5ef1060e39811ef875fa67027bd87b2f916cf7ad..a0411d5e92f2df41dcb0c8886ad1f45fe23bcc28 100644 GIT binary patch delta 9364 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 vtUNTE`iOWYH*&amu)O0e(9@e9> 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, - /// List of column names to be indexed (must match existing user-defined columns). - /// Indexes can target only user-defined columns; system columns ("id", "deleted", - /// "created_at", "row_revision") and automatically generated foreign key ("\*\_id") columns already - /// have indexes. Requests trying to index those columns are rejected. + /// Column names to index, matching names declared above. System columns + /// ("id", "deleted", "created_at", "row_revision") already have indexes, and + /// a LINK column is indexed when it is created. Requests naming either are + /// rejected. #[prost(string, repeated, tag = "4")] pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// Name of profile (Postgres schema) where the table will be created. @@ -106,9 +108,13 @@ pub struct AddTableColumnsRequest { #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ColumnDefinition { - /// Column name that follows the same validation rules as table_name. - /// Must be lowercase, start with a letter, no uppercase characters, - /// and cannot be "id", "deleted", "created_at", "row_revision", or end with "\_id". + /// Must be lowercase, start with a letter, and use only lowercase letters, + /// digits and underscores. + /// + /// Cannot be "id", "deleted", "created_at" or "row_revision": those are + /// system columns, and a data request names system and user columns in one + /// namespace. Any other name is free, including one ending in "\_id" -- no + /// column name is derived from a table name, so nothing collides. #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, /// Logical column type. Supported values (case-insensitive): diff --git a/common/src/proto/komp_ac.table_script.rs b/common/src/proto/komp_ac.table_script.rs index 6eef1846..2099cdb5 100644 --- a/common/src/proto/komp_ac.table_script.rs +++ b/common/src/proto/komp_ac.table_script.rs @@ -127,6 +127,18 @@ pub struct ScriptDependency { /// Relationship table used to match the owner row to the related collection. #[prost(string, tag = "5")] pub via_table: ::prost::alloc::string::String, + /// Column of the current row holding the referenced row's id: the link this + /// dependency travels, resolved when the script was saved. Empty when no link + /// is travelled -- a self-reference, or an aggregate, which follows a path + /// instead. A table may link to one target several times, so this is the only + /// thing that says which link was meant; never derive it from target_table. + #[prost(string, tag = "6")] + pub link_column: ::prost::alloc::string::String, + /// The first argument exactly as the script wrote it: a link alias, or the + /// target table's name where one link makes that unambiguous. The Steel + /// runtime looks its inputs up by this text. + #[prost(string, tag = "7")] + pub name_in_script: ::prost::alloc::string::String, } /// Identifies the active form row whose external Steel inputs must be hydrated. #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/common/src/proto/komp_ac.table_structure.rs b/common/src/proto/komp_ac.table_structure.rs index 6f66e7d1..1d17cade 100644 --- a/common/src/proto/komp_ac.table_structure.rs +++ b/common/src/proto/komp_ac.table_structure.rs @@ -26,7 +26,7 @@ pub struct GetTableStructureResponse { pub struct TableStructureResponse { /// Columns of the physical table, including system columns (id, deleted, /// created_at, row_revision), user-defined columns, and any foreign-key columns such as - /// "\\_id", plus the dedicated "account_id" column on + /// named by whoever declared each link, plus the dedicated "account_id" on /// ACCOUNTING-enabled tables. May be empty if the physical table is missing. #[prost(message, repeated, tag = "1")] pub columns: ::prost::alloc::vec::Vec, diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index 6835a69a..ef531db7 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -17,7 +17,7 @@ pub struct PostTableDataRequest { /// * User-defined columns from the table definition /// * System/FK columns: /// • "deleted" (BOOLEAN), optional; default FALSE if not provided - /// • "\\_id" (BIGINT) for each table link + /// • one BIGINT per link, named by whoever declared it /// • "account_id" (BIGINT) on ACCOUNTING-enabled tables /// /// Type expectations by SQL type: @@ -190,7 +190,7 @@ pub struct GetTableDataResponse { /// /// * id, deleted /// * all user-defined columns from the table definition - /// * FK columns named "\\_id" for each table link + /// * one column per link, named by whoever declared it /// * account_id for ACCOUNTING-enabled tables /// /// All values are returned as TEXT via col::TEXT and COALESCEed to empty string @@ -482,7 +482,7 @@ pub mod tables_data_client { /// * Validates profile and table definition /// * Returns all columns as strings (COALESCE(col::TEXT, '') AS col) /// including: id, deleted, row_revision, all user-defined columns, and FK columns - /// named "\\_id" for each table link, plus "account_id" on + /// named by whoever declared each link, plus "account_id" on /// ACCOUNTING-enabled tables /// * Fails with NOT_FOUND if record does not exist or is soft-deleted /// * If the physical table is missing but the definition exists, returns INTERNAL @@ -675,7 +675,7 @@ pub mod tables_data_server { /// * Validates profile and table definition /// * Returns all columns as strings (COALESCE(col::TEXT, '') AS col) /// including: id, deleted, row_revision, all user-defined columns, and FK columns - /// named "\\_id" for each table link, plus "account_id" on + /// named by whoever declared each link, plus "account_id" on /// ACCOUNTING-enabled tables /// * Fails with NOT_FOUND if record does not exist or is soft-deleted /// * If the physical table is missing but the definition exists, returns INTERNAL diff --git a/server b/server index 79e64a2b..c5ff2c81 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 79e64a2bd56f064354e4fdb77ac0e4a25184dbf7 +Subproject commit c5ff2c81f1857b9958ffde1737729e149eeac8a6 diff --git a/web/src/schema/mod.rs b/web/src/schema/mod.rs index 662491b9..776f3900 100644 --- a/web/src/schema/mod.rs +++ b/web/src/schema/mod.rs @@ -670,12 +670,11 @@ pub(crate) fn validate_identifier( "{label} may only use lowercase letters, digits and underscores." )); } + // Only the system columns are reserved. The `_id` suffix is free: no + // column name is derived from a table name any more, so it collides with + // nothing. if reject_table_reserved - && (value == "id" - || value == "deleted" - || value == "created_at" - || value == "row_revision" - || value.ends_with("_id")) + && matches!(value, "id" | "deleted" | "created_at" | "row_revision") { return Some(format!("{label} uses a reserved name.")); } @@ -1127,8 +1126,6 @@ pub(crate) mod tests { draft.name_input = "Total".to_string(); assert!(draft.add_from_inputs().is_err()); - draft.name_input = "customer_id".to_string(); - assert!(draft.add_from_inputs().is_err()); draft.name_input = "created_at".to_string(); assert!(draft.add_from_inputs().is_err()); @@ -1334,3 +1331,27 @@ pub(crate) mod tests { assert_eq!(draft.selected_index_names(), vec!["number"]); } } + +#[cfg(test)] +mod link_alias_tests { + use super::validate_identifier; + + /// The `_id` suffix is an ordinary part of a column's name: nothing is + /// derived from a table name any more. + #[test] + fn a_column_name_may_end_in_id() { + assert_eq!(validate_identifier("external_id", "Column name", true), None); + } + + /// The system columns stay reserved, since they share one namespace with + /// user columns in a data request. + #[test] + fn the_system_columns_stay_reserved() { + for name in ["id", "deleted", "created_at", "row_revision"] { + assert!( + validate_identifier(name, "Column name", true).is_some(), + "`{name}` must stay reserved" + ); + } + } +}