From d0b88777ae777a431b52865efff9046cd5e00b3c Mon Sep 17 00:00:00 2001 From: 20syldev Date: Mon, 20 Apr 2026 11:53:54 +0200 Subject: [PATCH] fix: correct FK direction when dragging from parent to child table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user drags from a primary key (one side) to a foreign key field (many side), the relationship was stored with the PK table as startTable. All SQL exports use startTable as the FK holder, generating the wrong ALTER TABLE statement. Fix A: normalize at creation time — when cardinality is ONE_TO_MANY, swap start/end so startTable is always the FK holder (many side), and set cardinality to MANY_TO_ONE. Fix B: safety net in all SQL exporters — use resolveFKDirection() to handle any pre-existing ONE_TO_MANY relationships in saved diagrams. Fixes #873 --- src/components/EditorCanvas/Canvas.jsx | 22 ++++-- src/utils/exportSQL/generic.js | 92 +++++++++++++------------- src/utils/exportSQL/mariadb.js | 20 +++--- src/utils/exportSQL/mssql.js | 21 +++--- src/utils/exportSQL/mysql.js | 25 ++++--- src/utils/exportSQL/oraclesql.js | 19 +++--- src/utils/exportSQL/postgres.js | 17 +++-- src/utils/exportSQL/shared.js | 23 +++++-- 8 files changed, 135 insertions(+), 104 deletions(-) diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index aed3af69a..b02daf27d 100644 --- a/src/components/EditorCanvas/Canvas.jsx +++ b/src/components/EditorCanvas/Canvas.jsx @@ -619,14 +619,28 @@ export default function Canvas() { const cardinality = getCardinality(startField, endField); + // Normalize direction: startTable must always be the FK holder (many side). + // When the user drags from the PK side (one) to the FK side (many), the + // result is ONE_TO_MANY with startTable = PK. Swap so that startTable = FK. + const isInverted = cardinality === Cardinality.ONE_TO_MANY; + const fkTableId = isInverted ? hoveredTable.tableId : linkingLine.startTableId; + const fkFieldId = isInverted ? hoveredTable.fieldId : linkingLine.startFieldId; + const refTableId = isInverted ? linkingLine.startTableId : hoveredTable.tableId; + const refFieldId = isInverted ? linkingLine.startFieldId : hoveredTable.fieldId; + const fkTableName = isInverted ? endTableName : startTableName; + const fkField = isInverted ? endField : startField; + const refTableName = isInverted ? startTableName : endTableName; + const newRelationship = { ...linkingLine, - cardinality, - endTableId: hoveredTable.tableId, - endFieldId: hoveredTable.fieldId, + cardinality: isInverted ? Cardinality.MANY_TO_ONE : cardinality, + startTableId: fkTableId, + startFieldId: fkFieldId, + endTableId: refTableId, + endFieldId: refFieldId, updateConstraint: Constraint.NONE, deleteConstraint: Constraint.NONE, - name: `fk_${startTableName}_${startField.name}_${endTableName}`, + name: `fk_${fkTableName}_${fkField.name}_${refTableName}`, id: nanoid(), }; delete newRelationship.startX; diff --git a/src/utils/exportSQL/generic.js b/src/utils/exportSQL/generic.js index 04d220839..78587e61f 100644 --- a/src/utils/exportSQL/generic.js +++ b/src/utils/exportSQL/generic.js @@ -1,6 +1,6 @@ import { DB } from "../../data/constants"; import { dbToTypes, defaultTypes } from "../../data/datatypes"; -import { escapeQuotes, getInlineFK, parseDefault } from "./shared"; +import { escapeQuotes, getInlineFK, parseDefault, resolveFKDirection } from "./shared"; export function getJsonType(f) { if (!Object.keys(defaultTypes).includes(f.type)) { @@ -229,17 +229,17 @@ export function jsonToMySQL(obj) { ) .join("\n")}\n${obj.references .map((r) => { - const { name: startName, fields: startFields } = obj.tables.find( - (t) => t.id === r.startTableId, + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const { name: fkName, fields: fkFields } = obj.tables.find( + (t) => t.id === fkTableId, ); - - const { name: endName, fields: endFields } = obj.tables.find( - (t) => t.id === r.endTableId, + const { name: refName, fields: refFields } = obj.tables.find( + (t) => t.id === refTableId, ); - return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ - startFields.find((f) => f.id === r.startFieldId).name - }\`) REFERENCES \`${endName}\`(\`${ - endFields.find((f) => f.id === r.endFieldId).name + return `ALTER TABLE \`${fkName}\`\nADD FOREIGN KEY(\`${ + fkFields.find((f) => f.id === fkFieldId).name + }\`) REFERENCES \`${refName}\`(\`${ + refFields.find((f) => f.id === refFieldId).name }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; }) .join("\n")}`; @@ -336,17 +336,17 @@ export function jsonToPostgreSQL(obj) { ) .join("\n")}\n${obj.references .map((r) => { - const { name: startName, fields: startFields } = obj.tables.find( - (t) => t.id === r.startTableId, + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const { name: fkName, fields: fkFields } = obj.tables.find( + (t) => t.id === fkTableId, ); - - const { name: endName, fields: endFields } = obj.tables.find( - (t) => t.id === r.endTableId, + const { name: refName, fields: refFields } = obj.tables.find( + (t) => t.id === refTableId, ); - return `ALTER TABLE "${startName}"\nADD FOREIGN KEY("${ - startFields.find((f) => f.id === r.startFieldId).name - }") REFERENCES "${endName}"("${ - endFields.find((f) => f.id === r.endFieldId).name + return `ALTER TABLE "${fkName}"\nADD FOREIGN KEY("${ + fkFields.find((f) => f.id === fkFieldId).name + }") REFERENCES "${refName}"("${ + refFields.find((f) => f.id === refFieldId).name }")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; }) .join("\n")}`; @@ -474,17 +474,17 @@ export function jsonToMariaDB(obj) { ) .join("\n")}\n${obj.references .map((r) => { - const { name: startName, fields: startFields } = obj.tables.find( - (t) => t.id === r.startTableId, + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const { name: fkName, fields: fkFields } = obj.tables.find( + (t) => t.id === fkTableId, ); - - const { name: endName, fields: endFields } = obj.tables.find( - (t) => t.id === r.endTableId, + const { name: refName, fields: refFields } = obj.tables.find( + (t) => t.id === refTableId, ); - return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ - startFields.find((f) => f.id === r.startFieldId).name - }\`) REFERENCES \`${endName}\`(\`${ - endFields.find((f) => f.id === r.endFieldId).name + return `ALTER TABLE \`${fkName}\`\nADD FOREIGN KEY(\`${ + fkFields.find((f) => f.id === fkFieldId).name + }\`) REFERENCES \`${refName}\`(\`${ + refFields.find((f) => f.id === refFieldId).name }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; }) .join("\n")}`; @@ -546,17 +546,17 @@ export function jsonToSQLServer(obj) { ) .join("\n")}\n${obj.references .map((r) => { - const { name: startName, fields: startFields } = obj.tables.find( - (t) => t.id === r.startTableId, + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const { name: fkName, fields: fkFields } = obj.tables.find( + (t) => t.id === fkTableId, ); - - const { name: endName, fields: endFields } = obj.tables.find( - (t) => t.id === r.endTableId, + const { name: refName, fields: refFields } = obj.tables.find( + (t) => t.id === refTableId, ); - return `ALTER TABLE [${startName}]\nADD FOREIGN KEY([${ - startFields.find((f) => f.id === r.startFieldId).name - }]) REFERENCES [${endName}]([${ - endFields.find((f) => f.id === r.endFieldId).name + return `ALTER TABLE [${fkName}]\nADD FOREIGN KEY([${ + fkFields.find((f) => f.id === fkFieldId).name + }]) REFERENCES [${refName}]([${ + refFields.find((f) => f.id === refFieldId).name }])\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};\nGO`; }) .join("\n")}`; @@ -619,17 +619,17 @@ export function jsonToOracleSQL(obj) { ) .join("\n\n")}\n${obj.references .map((r) => { - const { name: startName, fields: startFields } = obj.tables.find( - (t) => t.id === r.startTableId, + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const { name: fkName, fields: fkFields } = obj.tables.find( + (t) => t.id === fkTableId, ); - - const { name: endName, fields: endFields } = obj.tables.find( - (t) => t.id === r.endTableId, + const { name: refName, fields: refFields } = obj.tables.find( + (t) => t.id === refTableId, ); - return `ALTER TABLE "${startName}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ - startFields.find((f) => f.id === r.startFieldId).name - }") REFERENCES "${endName}"("${ - endFields.find((f) => f.id === r.endFieldId).name + return `ALTER TABLE "${fkName}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ + fkFields.find((f) => f.id === fkFieldId).name + }") REFERENCES "${refName}"("${ + refFields.find((f) => f.id === refFieldId).name }");`; }) .join("\n")}`; diff --git a/src/utils/exportSQL/mariadb.js b/src/utils/exportSQL/mariadb.js index 59255d411..456bcea27 100644 --- a/src/utils/exportSQL/mariadb.js +++ b/src/utils/exportSQL/mariadb.js @@ -1,4 +1,4 @@ -import { escapeQuotes, parseDefault } from "./shared"; +import { escapeQuotes, parseDefault, resolveFKDirection } from "./shared"; import { dbToTypes } from "../../data/datatypes"; import { DB } from "../../data/constants"; @@ -60,17 +60,17 @@ export function toMariaDB(diagram) { ) .join("\n")}\n${diagram.references .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const { name: fkName, fields: fkFields } = diagram.tables.find( + (t) => t.id === fkTableId, ); - - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, + const { name: refName, fields: refFields } = diagram.tables.find( + (t) => t.id === refTableId, ); - return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ - startFields.find((f) => f.id === r.startFieldId).name - }\`) REFERENCES \`${endName}\`(\`${ - endFields.find((f) => f.id === r.endFieldId).name + return `ALTER TABLE \`${fkName}\`\nADD FOREIGN KEY(\`${ + fkFields.find((f) => f.id === fkFieldId).name + }\`) REFERENCES \`${refName}\`(\`${ + refFields.find((f) => f.id === refFieldId).name }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; }) .join("\n")}`; diff --git a/src/utils/exportSQL/mssql.js b/src/utils/exportSQL/mssql.js index a2354d358..a37dcf672 100644 --- a/src/utils/exportSQL/mssql.js +++ b/src/utils/exportSQL/mssql.js @@ -1,4 +1,4 @@ -import { parseDefault, escapeQuotes } from "./shared"; +import { parseDefault, escapeQuotes, resolveFKDirection } from "./shared"; import { dbToTypes } from "../../data/datatypes"; import { DB } from "../../data/constants"; @@ -94,19 +94,20 @@ export function toMSSQL(diagram) { const referencesSql = diagram.references .map((r) => { - const startTable = diagram.tables.find((t) => t.id === r.startTableId); - const endTable = diagram.tables.find((t) => t.id === r.endTableId); + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const fkTable = diagram.tables.find((t) => t.id === fkTableId); + const refTable = diagram.tables.find((t) => t.id === refTableId); - if (!startTable || !endTable) return ""; + if (!fkTable || !refTable) return ""; - const startField = startTable.fields.find((f) => f.id === r.startFieldId); - const endField = endTable.fields.find((f) => f.id === r.endFieldId); + const fkField = fkTable.fields.find((f) => f.id === fkFieldId); + const refField = refTable.fields.find((f) => f.id === refFieldId); - if (!startField || !endField) return ""; + if (!fkField || !refField) return ""; - return `\nALTER TABLE [${startTable.name}] -ADD FOREIGN KEY([${startField.name}]) -REFERENCES [${endTable.name}]([${endField.name}]) + return `\nALTER TABLE [${fkTable.name}] +ADD FOREIGN KEY([${fkField.name}]) +REFERENCES [${refTable.name}]([${refField.name}]) ON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}; GO`; }) diff --git a/src/utils/exportSQL/mysql.js b/src/utils/exportSQL/mysql.js index f35dbda3f..637f4c452 100644 --- a/src/utils/exportSQL/mysql.js +++ b/src/utils/exportSQL/mysql.js @@ -1,7 +1,7 @@ import { escapeQuotes, parseDefault } from "./shared"; import { dbToTypes } from "../../data/datatypes"; -import { DB } from "../../data/constants"; +import { Cardinality, DB } from "../../data/constants"; function parseType(field) { let res = field.type; @@ -64,17 +64,22 @@ export function toMySQL(diagram) { ) .join("\n")}\n${diagram.references .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, - ); + const isInverted = r.cardinality === Cardinality.ONE_TO_MANY; + const fkTableId = isInverted ? r.endTableId : r.startTableId; + const fkFieldId = isInverted ? r.endFieldId : r.startFieldId; + const refTableId = isInverted ? r.startTableId : r.endTableId; + const refFieldId = isInverted ? r.startFieldId : r.endFieldId; - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, + const { name: fkName, fields: fkFields } = diagram.tables.find( + (t) => t.id === fkTableId, + ); + const { name: refName, fields: refFields } = diagram.tables.find( + (t) => t.id === refTableId, ); - return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ - startFields.find((f) => f.id === r.startFieldId).name - }\`) REFERENCES \`${endName}\`(\`${ - endFields.find((f) => f.id === r.endFieldId).name + return `ALTER TABLE \`${fkName}\`\nADD FOREIGN KEY(\`${ + fkFields.find((f) => f.id === fkFieldId).name + }\`) REFERENCES \`${refName}\`(\`${ + refFields.find((f) => f.id === refFieldId).name }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; }) .join("\n")}`; diff --git a/src/utils/exportSQL/oraclesql.js b/src/utils/exportSQL/oraclesql.js index 0a44c5650..811b04675 100644 --- a/src/utils/exportSQL/oraclesql.js +++ b/src/utils/exportSQL/oraclesql.js @@ -1,5 +1,5 @@ import { dbToTypes } from "../../data/datatypes"; -import { parseDefault } from "./shared"; +import { parseDefault, resolveFKDirection } from "./shared"; export function toOracleSQL(diagram) { return `${diagram.tables @@ -47,16 +47,17 @@ export function toOracleSQL(diagram) { ) .join("\n")}\n${diagram.references .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const { name: fkName, fields: fkFields } = diagram.tables.find( + (t) => t.id === fkTableId, ); - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, + const { name: refName, fields: refFields } = diagram.tables.find( + (t) => t.id === refTableId, ); - return `ALTER TABLE "${startName}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ - startFields.find((f) => f.id === r.startFieldId).name - }") REFERENCES "${endName}" ("${ - endFields.find((f) => f.id === r.endFieldId).name + return `ALTER TABLE "${fkName}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ + fkFields.find((f) => f.id === fkFieldId).name + }") REFERENCES "${refName}" ("${ + refFields.find((f) => f.id === refFieldId).name }")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; }) .join("\n")}`; diff --git a/src/utils/exportSQL/postgres.js b/src/utils/exportSQL/postgres.js index 3dc6c0e13..9d4cd2488 100644 --- a/src/utils/exportSQL/postgres.js +++ b/src/utils/exportSQL/postgres.js @@ -1,4 +1,4 @@ -import { escapeQuotes, exportFieldComment, parseDefault } from "./shared"; +import { escapeQuotes, exportFieldComment, parseDefault, resolveFKDirection } from "./shared"; import { dbToTypes } from "../../data/datatypes"; export function toPostgres(diagram) { @@ -87,16 +87,15 @@ export function toPostgres(diagram) { const foreignKeyStatements = diagram.references .map((r) => { - const startTable = diagram.tables.find((t) => t.id === r.startTableId); - const endTable = diagram.tables.find((t) => t.id === r.endTableId); - const startField = startTable?.fields.find( - (f) => f.id === r.startFieldId, - ); - const endField = endTable?.fields.find((f) => f.id === r.endFieldId); + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + const fkTable = diagram.tables.find((t) => t.id === fkTableId); + const refTable = diagram.tables.find((t) => t.id === refTableId); + const fkField = fkTable?.fields.find((f) => f.id === fkFieldId); + const refField = refTable?.fields.find((f) => f.id === refFieldId); - if (!startTable || !endTable || !startField || !endField) return ""; + if (!fkTable || !refTable || !fkField || !refField) return ""; - return `ALTER TABLE "${startTable.name}"\nADD FOREIGN KEY("${startField.name}") REFERENCES "${endTable.name}"("${endField.name}")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + return `ALTER TABLE "${fkTable.name}"\nADD FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; }) .filter(Boolean) .join("\n"); diff --git a/src/utils/exportSQL/shared.js b/src/utils/exportSQL/shared.js index 64100bc7f..11217aa67 100644 --- a/src/utils/exportSQL/shared.js +++ b/src/utils/exportSQL/shared.js @@ -1,6 +1,6 @@ import { isFunction, isKeyword } from "../utils"; -import { DB } from "../../data/constants"; +import { Cardinality, DB } from "../../data/constants"; import { dbToTypes } from "../../data/datatypes"; export function parseDefault(field, database = DB.GENERIC) { @@ -30,17 +30,28 @@ export function exportFieldComment(comment) { .join(""); } +export function resolveFKDirection(r) { + const isInverted = r.cardinality === Cardinality.ONE_TO_MANY; + return { + fkTableId: isInverted ? r.endTableId : r.startTableId, + fkFieldId: isInverted ? r.endFieldId : r.startFieldId, + refTableId: isInverted ? r.startTableId : r.endTableId, + refFieldId: isInverted ? r.startFieldId : r.endFieldId, + }; +} + export function getInlineFK(table, obj) { let fks = []; obj.references.forEach((r) => { - if (r.startTableId === table.id) { + const { fkTableId, fkFieldId, refTableId, refFieldId } = resolveFKDirection(r); + if (fkTableId === table.id) { fks.push( - `\tFOREIGN KEY ("${table.fields.find((f) => f.id === r.startFieldId)?.name}") REFERENCES "${ - obj.tables.find((t) => t.id === r.endTableId)?.name + `\tFOREIGN KEY ("${table.fields.find((f) => f.id === fkFieldId)?.name}") REFERENCES "${ + obj.tables.find((t) => t.id === refTableId)?.name }"("${ obj.tables - .find((t) => t.id === r.endTableId) - .fields.find((f) => f.id === r.endFieldId)?.name + .find((t) => t.id === refTableId) + .fields.find((f) => f.id === refFieldId)?.name }")\n\tON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}`, ); }