195 lines
7.2 KiB
Rust
195 lines
7.2 KiB
Rust
// 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";
|
|
/// Internal version of a managed row. It is never exposed as editable data.
|
|
pub const ROW_VERSION_COLUMN: &str = "version";
|
|
/// Records the user who closed an accounting journal. It is exposed only by
|
|
/// the dedicated accounting API, never by the generic table API.
|
|
pub const CLOSED_BY_USER_ID_COLUMN: &str = "closed_by_user_id";
|
|
|
|
/// Fixed server-owned columns that generic table APIs must omit entirely.
|
|
pub const INTERNAL_COLUMN_NAMES: [&str; 2] = [ROW_VERSION_COLUMN, CLOSED_BY_USER_ID_COLUMN];
|
|
|
|
/// 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
|
|
}
|
|
|
|
const fn longest_name(names: &[&str], mut longest: usize) -> usize {
|
|
let mut index = 0;
|
|
while index < names.len() {
|
|
let length = names[index].len();
|
|
if length > longest {
|
|
longest = length;
|
|
}
|
|
index += 1;
|
|
}
|
|
longest
|
|
}
|
|
|
|
let longest = longest_name(&INTERNAL_COLUMN_NAMES, ACCOUNT_REFERENCE_COLUMN.len());
|
|
let longest = longest_of(&LEADING_SYSTEM_COLUMNS, longest);
|
|
longest_of(&TRAILING_SYSTEM_COLUMNS, longest)
|
|
}
|
|
|
|
/// Fixed internal column names. Link-version columns are also internal, but
|
|
/// their generated names are loaded from table metadata at runtime.
|
|
pub fn internal_column_names() -> impl Iterator<Item = &'static str> {
|
|
INTERNAL_COLUMN_NAMES.into_iter()
|
|
}
|
|
|
|
pub fn is_internal_column(name: &str) -> bool {
|
|
internal_column_names().any(|internal_name| internal_name == name)
|
|
}
|
|
|
|
/// 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<Item = &'static str> {
|
|
LEADING_SYSTEM_COLUMNS
|
|
.iter()
|
|
.chain(TRAILING_SYSTEM_COLUMNS.iter())
|
|
.map(|column| column.name)
|
|
.chain(std::iter::once(ACCOUNT_REFERENCE_COLUMN))
|
|
.chain(internal_column_names())
|
|
}
|
|
|
|
/// Whether `name` belongs to the reserved system column vocabulary. Internal
|
|
/// columns must still be removed before a row or structure reaches a client.
|
|
/// 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::<Vec<_>>()
|
|
.join(", ")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
ACCOUNT_API_COLUMN, ACCOUNT_REFERENCE_COLUMN, CLOSED_BY_USER_ID_COLUMN,
|
|
LONGEST_SYSTEM_COLUMN_NAME, ROW_VERSION_COLUMN, is_internal_column, 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 internal_columns_are_reserved_but_not_public() {
|
|
for name in [ROW_VERSION_COLUMN, CLOSED_BY_USER_ID_COLUMN] {
|
|
assert!(is_system_column(name));
|
|
assert!(is_internal_column(name));
|
|
}
|
|
assert!(!is_internal_column(ACCOUNT_REFERENCE_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', 'version', 'closed_by_user_id', 'account'"
|
|
);
|
|
}
|
|
}
|