multirow FK
This commit is contained in:
@@ -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<LinkDefinition>,
|
||||
/// Tables in the target profile, offered as `link(...)` targets by the
|
||||
/// column picker.
|
||||
pub relation_tables: Vec<String>,
|
||||
/// 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<String>,
|
||||
@@ -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<String>) {
|
||||
let previous_modes = self
|
||||
.links
|
||||
.iter()
|
||||
.map(|link| (link.linked_table_name.clone(), link.mode))
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
|
||||
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]");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user