// common/src/system_column.rs //! The columns the server puts on every managed table. //! //! They are the one exception to the rule that a physical column name never //! leaves the server. A user column is stored under its ordinal and shown only //! under its alias, so an unmapped name at the public boundary is a leak; these //! have no alias to hide behind, because the server -- not the user -- chose //! their names. The list lives here once and the `CREATE TABLE` fragments that //! make it true live next to it. Alias validation may still be conditional: the //! virtual `account` API name conflicts only on ACCOUNTING-enabled tables. /// A column every managed table carries, named the same in Postgres and in the /// public API. pub struct SystemColumn { pub name: &'static str, /// The `CREATE TABLE` fragment that declares it. pub definition: &'static str, } /// Declared ahead of the user columns. pub const LEADING_SYSTEM_COLUMNS: [SystemColumn; 3] = [ SystemColumn { name: "id", definition: "id BIGSERIAL PRIMARY KEY", }, SystemColumn { name: "deleted", definition: "deleted BOOLEAN NOT NULL DEFAULT FALSE", }, SystemColumn { name: "row_revision", definition: "row_revision BIGINT NOT NULL DEFAULT 1", }, ]; /// Declared after the user columns. pub const TRAILING_SYSTEM_COLUMNS: [SystemColumn; 1] = [SystemColumn { name: "created_at", definition: "created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP", }]; /// Present only on a table that references its profile's chart of accounts. /// Its declaration names that profile's schema, so it is built where the /// profile is known rather than spelled out here. pub const ACCOUNT_REFERENCE_COLUMN: &str = "account_id"; pub const ACCOUNT_API_COLUMN: &str = "account"; /// The longest name any system column carries physically. /// /// Anything that names a database object after a column has to reserve room /// for the widest name it could be given, and a user column's ordinal is far /// shorter than these. The bound is computed from the declarations above /// rather than picked by hand, so adding a system column moves it. /// /// [`ACCOUNT_API_COLUMN`] is deliberately absent: it is an API spelling, never /// a physical column, so nothing is ever named after it. pub const LONGEST_SYSTEM_COLUMN_NAME: usize = longest_system_column_name(); const fn longest_system_column_name() -> usize { const fn longest_of(columns: &[SystemColumn], mut longest: usize) -> usize { let mut index = 0; while index < columns.len() { let length = columns[index].name.len(); if length > longest { longest = length; } index += 1; } longest } let longest = longest_of(&LEADING_SYSTEM_COLUMNS, ACCOUNT_REFERENCE_COLUMN.len()); longest_of(&TRAILING_SYSTEM_COLUMNS, longest) } /// Every system column name, whether or not the column is on a given table. /// /// A name is reserved for all tables even when only some tables carry the /// column, so an alias means the same thing everywhere. pub fn system_column_names() -> impl Iterator { LEADING_SYSTEM_COLUMNS .iter() .chain(TRAILING_SYSTEM_COLUMNS.iter()) .map(|column| column.name) .chain(std::iter::once(ACCOUNT_REFERENCE_COLUMN)) } /// Whether `name` belongs to the system column vocabulary and is safe to show /// to a client as-is. Whether a user may claim a virtual API name as an alias /// is decided with the table's capabilities in scope. pub fn is_system_column(name: &str) -> bool { name == ACCOUNT_API_COLUMN || system_column_names().any(|system_name| system_name == name) } /// The system column names in a message, as `'id', 'deleted', ...`. pub fn system_column_name_list() -> String { system_column_names() .chain(std::iter::once(ACCOUNT_API_COLUMN)) .map(|name| format!("'{name}'")) .collect::>() .join(", ") } #[cfg(test)] mod tests { use super::{ ACCOUNT_API_COLUMN, ACCOUNT_REFERENCE_COLUMN, LONGEST_SYSTEM_COLUMN_NAME, is_system_column, system_column_name_list, system_column_names, }; #[test] fn a_declared_column_is_a_system_column() { for name in system_column_names() { assert!(is_system_column(name), "{name} is declared but not public"); } } #[test] fn conditional_system_names_are_recognized() { assert!(is_system_column(ACCOUNT_REFERENCE_COLUMN)); assert!(is_system_column(ACCOUNT_API_COLUMN)); } #[test] fn an_ordinal_is_not_a_system_column() { assert!(!is_system_column("1")); assert!(!is_system_column("column_1")); } /// The const derivation walks the arrays directly, so this walks the /// public iterator instead. A system column declared somewhere the const /// function does not reach shows up here as a disagreement rather than as /// a truncated index name much later. #[test] fn the_longest_name_bound_covers_every_system_column() { let longest = system_column_names() .map(str::len) .max() .expect("there is always at least one system column"); assert_eq!(longest, LONGEST_SYSTEM_COLUMN_NAME); } #[test] fn the_name_list_reads_as_a_sentence_fragment() { assert_eq!( system_column_name_list(), "'id', 'deleted', 'row_revision', 'created_at', 'account_id', 'account'" ); } }