- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 } : {}} | ||||||
> | ||||||
<Input | ||||||
value={data.name} | ||||||
id={`scroll_table_${tid}_input_${index}`} | ||||||
validateStatus={data.name.trim() === "" ? "error" : "default"} | ||||||
validateStatus={ | ||||||
data.name.trim() === "" && !inherited ? "error" : "default" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
} | ||||||
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" } : {}} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
/> | ||||||
{inherited && ( | ||||||
<span style={{ fontSize: 12, color: "#888" }}>Inherited</span> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
|
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 && ( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use translation please
Suggested change
|
||||||
<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%" }} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 && ( | ||||||
|
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"] }, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how can it be an integer?
Suggested change
|
||||||
}, | ||||||
required: ["id", "name", "x", "y", "fields", "comment", "indices", "color"], | ||||||
}; | ||||||
|
||||||
|
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}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
}) | ||
.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"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.