diff --git a/src/components/EditorSidePanel/TablesTab/TableField.jsx b/src/components/EditorSidePanel/TablesTab/TableField.jsx index 7c744f421..be8f32f0f 100644 --- a/src/components/EditorSidePanel/TablesTab/TableField.jsx +++ b/src/components/EditorSidePanel/TablesTab/TableField.jsx @@ -8,7 +8,7 @@ import { dbToTypes } from "../../../data/datatypes"; import { DragHandle } from "../../SortableList/DragHandle"; import FieldDetails from "./FieldDetails"; -export default function TableField({ data, tid, index }) { +export default function TableField({ data, tid, index, inherited }) { const { updateField } = useDiagram(); const { types } = useTypes(); const { enums } = useEnums(); @@ -21,11 +21,16 @@ export default function TableField({ data, tid, index }) { return (
-
+
updateField(tid, data.id, { name: value })} onFocus={(e) => setEditField({ name: e.target.value })} @@ -49,7 +54,12 @@ export default function TableField({ data, tid, index }) { ]); setRedoStack([]); }} + readOnly={inherited} + style={inherited ? { backgroundColor: "#f5f5f5" } : {}} /> + {inherited && ( + Inherited + )}
t.id !== data.id) + .map((t) => ({ label: t.name, value: t.name }))} + onChange={(value) => updateTable(data.id, { inherits: value })} + placeholder="Select parent tables" + style={{ width: "100%" }} + /> +
+ )}
{t("name")}:
setSaveState(State.SAVING)} renderItem={(item, i) => ( - + )} /> {data.indices.length > 0 && ( diff --git a/src/data/schemas.js b/src/data/schemas.js index b8c112c4f..1946a9259 100644 --- a/src/data/schemas.js +++ b/src/data/schemas.js @@ -56,6 +56,10 @@ export const tableSchema = { }, color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" }, }, + inherits: { + type: "array", + items: { type: ["string", "integer"] }, + }, required: ["id", "name", "x", "y", "fields", "comment", "indices", "color"], }; diff --git a/src/utils/exportSQL/postgres.js b/src/utils/exportSQL/postgres.js index c4b7b1289..64641e307 100644 --- a/src/utils/exportSQL/postgres.js +++ b/src/utils/exportSQL/postgres.js @@ -1,12 +1,13 @@ import { escapeQuotes, exportFieldComment, parseDefault } from "./shared"; - import { dbToTypes } from "../../data/datatypes"; export function toPostgres(diagram) { const enumStatements = diagram.enums .map( (e) => - `CREATE TYPE "${e.name}" AS ENUM (\n${e.values.map((v) => `\t'${v}'`).join(",\n")}\n);\n`, + `CREATE TYPE "${e.name}" AS ENUM (\n${e.values + .map((v) => `\t'${v}'`) + .join(",\n")}\n);\n` ) .join("\n"); @@ -16,81 +17,108 @@ export function toPostgres(diagram) { `CREATE TYPE ${type.name} AS (\n${type.fields .map((f) => `\t${f.name} ${f.type}`) .join(",\n")}\n);\n\n${ - type.comment && type.comment.trim() !== "" - ? `\nCOMMENT ON TYPE "${type.name}" IS '${escapeQuotes(type.comment)}';\n\n` + type.comment?.trim() + ? `COMMENT ON TYPE "${type.name}" IS '${escapeQuotes(type.comment)}';\n` : "" - }`, + }` ) .join("\n"); - return `${enumStatements}${enumStatements.trim() !== "" ? `\n${typeStatements}` : typeStatements}${diagram.tables - .map( - (table) => - `CREATE TABLE "${table.name}" (\n${table.fields - .map( - (field) => - `${exportFieldComment(field.comment)}\t"${ - field.name - }" ${field.type}${ - field.size !== undefined && field.size !== "" - ? "(" + field.size + ")" - : "" - }${field.isArray ? " ARRAY" : ""}${field.notNull ? " NOT NULL" : ""}${field.unique ? " UNIQUE" : ""}${ - field.increment ? " GENERATED BY DEFAULT AS IDENTITY" : "" - }${ - field.default.trim() !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `"${f.name}"`) - .join(", ")})` - : "" - }\n);${ - table.comment.trim() !== "" - ? `\nCOMMENT ON TABLE "${table.name}" IS '${escapeQuotes(table.comment)}';\n` - : "" - }${table.fields + const tableStatements = diagram.tables + .map((table) => { + const inheritsClause = + Array.isArray(table.inherits) && table.inherits.length > 0 + ? `\n) INHERITS (${table.inherits.map((parent) => `"${parent}"`).join(", ")})` + : ")"; + + const inheritedFieldNames = Array.from( + new Set( + (Array.isArray(table.inherits) ? table.inherits : []) + .map((parentName) => { + const parent = diagram.tables.find((t) => t.name === parentName); + return parent ? parent.fields.map((f) => f.name) : []; + }) + .flat() + ) + ); + + const ownFields = table.fields.filter((f) => !inheritedFieldNames.includes(f.name)); + + const fieldDefinitions = ownFields + .map( + (field) => + `${exportFieldComment(field.comment)}\t"${ + field.name + }" ${field.type}${ + field.size ? `(${field.size})` : "" + }${field.isArray ? " ARRAY" : ""}${field.notNull ? " NOT NULL" : ""}${field.unique ? " UNIQUE" : ""}${ + field.increment ? " GENERATED BY DEFAULT AS IDENTITY" : "" + }${ + field.default?.trim() + ? ` DEFAULT ${parseDefault(field, diagram.database)}` + : "" + }${ + field.check && + dbToTypes[diagram.database][field.type]?.hasCheck + ? ` CHECK(${field.check})` + : "" + }` + ) + .join(",\n"); + + const primaryKeyClause = ownFields.some((f) => f.primary) + ? `,\n\tPRIMARY KEY(${ownFields + .filter((f) => f.primary) + .map((f) => `"${f.name}"`) + .join(", ")})` + : ""; + + const commentStatements = [ + table.comment?.trim() + ? `COMMENT ON TABLE "${table.name}" IS '${escapeQuotes(table.comment)}';` + : "", + ...ownFields .map((field) => - field.comment.trim() !== "" - ? `COMMENT ON COLUMN ${table.name}.${field.name} IS '${escapeQuotes(field.comment)}';\n` - : "", + field.comment?.trim() + ? `COMMENT ON COLUMN "${table.name}"."${field.name}" IS '${escapeQuotes(field.comment)}';` + : "" ) - .join("")}${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${ - i.name - }"\nON "${table.name}" (${i.fields - .map((f) => `"${f}"`) - .join(", ")});`, - ) - .join("\n")}\n`, - ) - .join("\n")}${diagram.references + .filter(Boolean), + ].join("\n"); + + const indexStatements = table.indices + .map( + (i) => + `CREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields + .map((f) => `"${f}"`) + .join(", ")});` + ) + .join("\n"); + + return `CREATE TABLE "${table.name}" (\n${fieldDefinitions}${primaryKeyClause}${inheritsClause};\n${commentStatements}\n${indexStatements}`; + }) + .join("\n\n"); + + const foreignKeyStatements = diagram.references .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, - ); + 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 { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, - ); + if (!startTable || !endTable || !startField || !endField) return ""; - return `\nALTER TABLE "${startName}"\nADD FOREIGN KEY("${ - startFields.find((f) => f.id === r.startFieldId)?.name - }") REFERENCES "${endName}"("${ - endFields.find((f) => f.id === r.endFieldId)?.name - }")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + 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()};`; }) - .join("\n")}`; + .filter(Boolean) + .join("\n"); + + return [ + enumStatements, + enumStatements.trim() && typeStatements ? "\n" + typeStatements : typeStatements, + tableStatements, + foreignKeyStatements, + ] + .filter(Boolean) + .join("\n\n"); }