Skip to content

feat: add support for PostgreSQL table inheritance in schema export #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/components/EditorSidePanel/TablesTab/TableField.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="hover-1 my-2 flex gap-2 items-center">
<DragHandle id={data.id} />
<div className="min-w-20 flex-1/3">
<div
className="min-w-20 flex-1/3"
style={inherited ? { opacity: 0.6 } : {}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
style={inherited ? { opacity: 0.6 } : {}}

>
<Input
value={data.name}
id={`scroll_table_${tid}_input_${index}`}
validateStatus={data.name.trim() === "" ? "error" : "default"}
validateStatus={
data.name.trim() === "" && !inherited ? "error" : "default"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

names still shouldnt be empty and overriding an inherited fields should result in an error

Suggested change
data.name.trim() === "" && !inherited ? "error" : "default"
data.name.trim() === "" || inherited ? "error" : "default"

}
placeholder="Name"
onChange={(value) => 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" } : {}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

causes
image

Suggested change
style={inherited ? { backgroundColor: "#f5f5f5" } : {}}

/>
{inherited && (
<span style={{ fontSize: 12, color: "#888" }}>Inherited</span>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when an inherited field gets overridden an error should be reported, instead of showing this here please add an issue in issues.js

)}
</div>
<div className="min-w-24 flex-1/3">
<Select
35 changes: 34 additions & 1 deletion src/components/EditorSidePanel/TablesTab/TableInfo.jsx
Original file line number Diff line number Diff line change
@@ -15,17 +15,45 @@ import IndexDetails from "./IndexDetails";
import { useTranslation } from "react-i18next";
import { SortableList } from "../../SortableList/SortableList";
import { nanoid } from "nanoid";
import { Select } from "@douyinfe/semi-ui";
import { DB } from "../../../data/constants";

export default function TableInfo({ data }) {
const { tables, database } = useDiagram();
const { t } = useTranslation();
const [indexActiveKey, setIndexActiveKey] = useState("");
const { deleteTable, updateTable, setTables } = useDiagram();
const { setUndoStack, setRedoStack } = useUndoRedo();
const { setSaveState } = useSaveState();
const [editField, setEditField] = useState({});
// Get inherited field names from parent tables
const inheritedFieldNames =
Array.isArray(data.inherits) && data.inherits.length > 0
? data.inherits
.map((parentName) => {
const parent = tables.find((t) => t.name === parentName);
return parent ? parent.fields.map((f) => f.name) : [];
})
.flat()
: [];

return (
<div>
{database === DB.POSTGRES && (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this below the tablefield list above comments

the table info should start with the most characteristic attributes such as name and columns

<div className="mb-2">
<div className="text-md font-semibold break-keep">Inherits:</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use translation please

Suggested change
<div className="text-md font-semibold break-keep">Inherits:</div>
<div className="text-md font-semibold break-keep">{t('inherits')}:</div>

<Select
multiple
value={data.inherits || []}
optionList={tables
.filter((t) => 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%" }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tailwind please

/>
</div>
)}
<div className="flex items-center mb-2.5">
<div className="text-md font-semibold break-keep">{t("name")}: </div>
<Input
@@ -68,7 +96,12 @@ export default function TableInfo({ data }) {
}}
afterChange={() => setSaveState(State.SAVING)}
renderItem={(item, i) => (
<TableField data={item} tid={data.id} index={i} />
<TableField
data={item}
tid={data.id}
index={i}
inherited={inheritedFieldNames.includes(item.name)}
/>
)}
/>
{data.indices.length > 0 && (
4 changes: 4 additions & 0 deletions src/data/schemas.js
Original file line number Diff line number Diff line change
@@ -56,6 +56,10 @@ export const tableSchema = {
},
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" },
},
inherits: {
type: "array",
items: { type: ["string", "integer"] },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can it be an integer?

Suggested change
items: { type: ["string", "integer"] },
items: { type: ["string"] },

},
required: ["id", "name", "x", "y", "fields", "comment", "indices", "color"],
};

166 changes: 97 additions & 69 deletions src/utils/exportSQL/postgres.js
Original file line number Diff line number Diff line change
@@ -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}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

please have the closing parenthesis be on a new line

})
.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");
}