refactor: relationship model to column lists for compound FK (ADR-0043)
Move the FK column fields String->Vec<String> through all six layers (AddRelationship/SqlForeignKey AST, RelationshipSchema, metadata, project.yaml, ReadForeignKey, RelationshipEnd). Metadata stores comma-joined lists in the existing TEXT cells; project.yaml endpoints now columns: [a, b] (house style). Executor logic is multi-column ready: resolve_fk_parent_columns (full-PK F-A + auto-expand F-D), per-pair type-compat, schema_to_ddl multi-column emission, pragma FK read grouped by id, auto-name + --create-fk per-column, multi-column teaching echo. Single-column behaviour preserved (one-element vecs); all 2181 tests green. The grammar to parse multi-column input lands next.
This commit is contained in:
@@ -245,9 +245,13 @@ pub struct IndexSchema {
|
||||
pub struct RelationshipSchema {
|
||||
pub name: String,
|
||||
pub parent_table: String,
|
||||
pub parent_column: String,
|
||||
/// Parent PK column(s); one element for single-column, ordered
|
||||
/// list for a compound-PK FK (ADR-0043). Paired positionally
|
||||
/// with `child_columns`.
|
||||
pub parent_columns: Vec<String>,
|
||||
pub child_table: String,
|
||||
pub child_column: String,
|
||||
/// Child column(s), positionally paired with `parent_columns`.
|
||||
pub child_columns: Vec<String>,
|
||||
pub on_delete: ReferentialAction,
|
||||
pub on_update: ReferentialAction,
|
||||
}
|
||||
|
||||
+27
-13
@@ -188,20 +188,31 @@ fn write_relationship(out: &mut String, rel: &RelationshipSchema) {
|
||||
let _ = writeln!(out, " - name: {}", quote_if_needed(&rel.name));
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" parent: {{ table: {}, column: {} }}",
|
||||
" parent: {{ table: {}, columns: [{}] }}",
|
||||
quote_if_needed(&rel.parent_table),
|
||||
quote_if_needed(&rel.parent_column),
|
||||
write_col_list(&rel.parent_columns),
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" child: {{ table: {}, column: {} }}",
|
||||
" child: {{ table: {}, columns: [{}] }}",
|
||||
quote_if_needed(&rel.child_table),
|
||||
quote_if_needed(&rel.child_column),
|
||||
write_col_list(&rel.child_columns),
|
||||
);
|
||||
let _ = writeln!(out, " on_delete: {}", action_keyword(rel.on_delete));
|
||||
let _ = writeln!(out, " on_update: {}", action_keyword(rel.on_update));
|
||||
}
|
||||
|
||||
/// Format a column list for an inline yaml flow sequence — `a, b`
|
||||
/// (the caller wraps in `[…]`), each element quoted if needed.
|
||||
/// Matches the `primary_key: [...]` / index `columns: [...]` house
|
||||
/// style (ADR-0043 D5). One element for a single-column endpoint.
|
||||
fn write_col_list(cols: &[String]) -> String {
|
||||
cols.iter()
|
||||
.map(|c| quote_if_needed(c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
const fn action_keyword(action: ReferentialAction) -> &'static str {
|
||||
match action {
|
||||
ReferentialAction::NoAction => "no_action",
|
||||
@@ -309,9 +320,9 @@ pub(crate) fn parse_schema(body: &str) -> Result<SchemaSnapshot, YamlError> {
|
||||
relationships.push(RelationshipSchema {
|
||||
name: r.name,
|
||||
parent_table: r.parent.table,
|
||||
parent_column: r.parent.column,
|
||||
parent_columns: r.parent.columns,
|
||||
child_table: r.child.table,
|
||||
child_column: r.child.column,
|
||||
child_columns: r.child.columns,
|
||||
on_delete,
|
||||
on_update,
|
||||
});
|
||||
@@ -502,7 +513,10 @@ struct RawRelationship {
|
||||
#[derive(Deserialize)]
|
||||
struct RawEndpoint {
|
||||
table: String,
|
||||
column: String,
|
||||
/// FK endpoint column list (ADR-0043): `columns: [a, b]`, one
|
||||
/// element for a single-column endpoint — matching the
|
||||
/// `primary_key` / index `columns` house style.
|
||||
columns: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -551,9 +565,9 @@ mod tests {
|
||||
relationships: vec![RelationshipSchema {
|
||||
name: "Customers_id_to_Orders_CustId".to_string(),
|
||||
parent_table: "Customers".to_string(),
|
||||
parent_column: "id".to_string(),
|
||||
parent_columns: vec!["id".to_string()],
|
||||
child_table: "Orders".to_string(),
|
||||
child_column: "CustId".to_string(),
|
||||
child_columns: vec!["CustId".to_string()],
|
||||
on_delete: ReferentialAction::Cascade,
|
||||
on_update: ReferentialAction::NoAction,
|
||||
}],
|
||||
@@ -578,8 +592,8 @@ mod tests {
|
||||
assert!(body.contains("{ name: id, type: serial }"));
|
||||
assert!(body.contains("{ name: Name, type: text }"));
|
||||
assert!(body.contains("- name: Customers_id_to_Orders_CustId"));
|
||||
assert!(body.contains("parent: { table: Customers, column: id }"));
|
||||
assert!(body.contains("child: { table: Orders, column: CustId }"));
|
||||
assert!(body.contains("parent: { table: Customers, columns: [id] }"));
|
||||
assert!(body.contains("child: { table: Orders, columns: [CustId] }"));
|
||||
assert!(body.contains("on_delete: cascade"));
|
||||
assert!(body.contains("on_update: no_action"));
|
||||
assert!(body.contains("- name: Orders_CustId_idx"));
|
||||
@@ -934,8 +948,8 @@ project:
|
||||
tables: []
|
||||
relationships:
|
||||
- name: R
|
||||
parent: { table: A, column: id }
|
||||
child: { table: B, column: aid }
|
||||
parent: { table: A, columns: [id] }
|
||||
child: { table: B, columns: [aid] }
|
||||
on_delete: blow_up
|
||||
on_update: no_action
|
||||
";
|
||||
|
||||
Reference in New Issue
Block a user