centralized system columns - BUG NEEDS FIX FOR SPECIFIC SYSTEM COLUMNS
This commit is contained in:
@@ -4,6 +4,7 @@ pub mod search;
|
|||||||
pub mod decimal;
|
pub mod decimal;
|
||||||
pub mod grpc_error;
|
pub mod grpc_error;
|
||||||
pub mod relationship;
|
pub mod relationship;
|
||||||
|
pub mod system_column;
|
||||||
|
|
||||||
pub mod proto {
|
pub mod proto {
|
||||||
pub mod komp_ac {
|
pub mod komp_ac {
|
||||||
|
|||||||
104
common/src/system_column.rs
Normal file
104
common/src/system_column.rs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// 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. Anything asking "is this name public?" or "may a user claim
|
||||||
|
//! this name?" is asking about this list, so the list lives here once and the
|
||||||
|
//! `CREATE TABLE` fragments that make it true live next to it.
|
||||||
|
|
||||||
|
/// 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";
|
||||||
|
|
||||||
|
/// 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `name` is a system column: a name that is safe to show a client
|
||||||
|
/// as-is, and that a user may not claim for a column alias or a table.
|
||||||
|
pub fn is_system_column(name: &str) -> bool {
|
||||||
|
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()
|
||||||
|
.map(|name| format!("'{name}'"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
ACCOUNT_REFERENCE_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 a_conditional_column_is_reserved_on_every_table() {
|
||||||
|
assert!(is_system_column(ACCOUNT_REFERENCE_COLUMN));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_ordinal_is_not_a_system_column() {
|
||||||
|
assert!(!is_system_column("1"));
|
||||||
|
assert!(!is_system_column("column_1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_name_list_reads_as_a_sentence_fragment() {
|
||||||
|
assert_eq!(
|
||||||
|
system_column_name_list(),
|
||||||
|
"'id', 'deleted', 'row_revision', 'created_at', 'account_id'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use common::proto::komp_ac::search::{
|
|||||||
SearchOrderDirection, SearchRequest, SearchResponse, search_response::Hit,
|
SearchOrderDirection, SearchRequest, SearchResponse, search_response::Hit,
|
||||||
};
|
};
|
||||||
use common::search::{SchemaFields, register_tokenizers, search_index_path};
|
use common::search::{SchemaFields, register_tokenizers, search_index_path};
|
||||||
|
use common::system_column::is_system_column;
|
||||||
use query_builder::{
|
use query_builder::{
|
||||||
ConstraintMode, SearchConstraint, SearchConstraintTarget, build_master_query,
|
ConstraintMode, SearchConstraint, SearchConstraintTarget, build_master_query,
|
||||||
};
|
};
|
||||||
@@ -536,7 +537,7 @@ fn remap_json_to_display_names(
|
|||||||
for (key, value) in object {
|
for (key, value) in object {
|
||||||
let final_key = match physical_to_display.get(&key) {
|
let final_key = match physical_to_display.get(&key) {
|
||||||
Some(display_name) => display_name.clone(),
|
Some(display_name) => display_name.clone(),
|
||||||
None if is_public_system_column(&key) => key,
|
None if is_system_column(&key) => key,
|
||||||
None => {
|
None => {
|
||||||
return Err(Status::failed_precondition(
|
return Err(Status::failed_precondition(
|
||||||
"A table column has no public alias mapping",
|
"A table column has no public alias mapping",
|
||||||
@@ -551,13 +552,6 @@ fn remap_json_to_display_names(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_public_system_column(name: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
name,
|
|
||||||
"id" | "deleted" | "created_at" | "row_revision" | "account_id"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One value per display column, positionally aligned with them, so a column
|
/// One value per display column, positionally aligned with them, so a column
|
||||||
/// that is NULL for this row stays visible as an empty slot.
|
/// that is NULL for this row stays visible as an empty slot.
|
||||||
fn row_display_values(value: &serde_json::Value, columns: &[String]) -> Vec<String> {
|
fn row_display_values(value: &serde_json::Value, columns: &[String]) -> Vec<String> {
|
||||||
@@ -613,7 +607,7 @@ async fn resolve_order_column(
|
|||||||
.find(|(_, display)| display.eq_ignore_ascii_case(&requested_column))
|
.find(|(_, display)| display.eq_ignore_ascii_case(&requested_column))
|
||||||
.map(|(physical, _)| physical.clone())
|
.map(|(physical, _)| physical.clone())
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
is_public_system_column(&requested_column).then(|| requested_column.clone())
|
is_system_column(&requested_column).then(|| requested_column.clone())
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
Status::invalid_argument(format!(
|
Status::invalid_argument(format!(
|
||||||
|
|||||||
2
server
2
server
Submodule server updated: 585ae28e4f...1fe9ffa759
Reference in New Issue
Block a user