diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index 593e69d69..d7c016683 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -73,6 +73,8 @@ import { jsonToMermaid } from "../../utils/exportAs/mermaid"; import { isRtl } from "../../i18n/utils/rtl"; import { jsonToDocumentation } from "../../utils/exportAs/documentation"; import { IdContext } from "../Workspace"; +import DatabasesSwitcher from "./DatabasesSwitcher"; +import { convertTableSchema } from "../../utils/typesMappings"; export default function ControlPanel({ diagramId, @@ -80,6 +82,7 @@ export default function ControlPanel({ title, setTitle, lastSaved, + setLastSaved, }) { const [modal, setModal] = useState(MODAL.NONE); const [sidesheet, setSidesheet] = useState(SIDESHEET.NONE); @@ -107,6 +110,7 @@ export default function ControlPanel({ deleteRelationship, database, } = useDiagram(); + const [prevDatabase, setPrevDatabase] = useState(database); const { enums, setEnums, deleteEnum, addEnum, updateEnum } = useEnums(); const { types, addType, deleteType, updateType, setTypes } = useTypes(); const { notes, setNotes, updateNote, addNote, deleteNote } = useNotes(); @@ -923,8 +927,9 @@ export default function ControlPanel({ function: () => { if (database === DB.GENERIC) return; setModal(MODAL.CODE); + const newTables = tables.map(table => convertTableSchema(table, prevDatabase, database)); const src = exportSQL({ - tables: tables, + tables: newTables, references: relationships, types: types, database: database, @@ -1640,6 +1645,11 @@ export default function ControlPanel({ title={databases[database].name + " diagram"} /> )} + <DatabasesSwitcher + setLastSaved={setLastSaved} + diagramId={diagramId} + setPrevDatabase={setPrevDatabase} + /> <div className="text-xl me-1" onPointerEnter={(e) => e.isPrimary && setShowEditName(true)} diff --git a/src/components/EditorHeader/DatabasesSwitcher.jsx b/src/components/EditorHeader/DatabasesSwitcher.jsx new file mode 100644 index 000000000..2f647ecff --- /dev/null +++ b/src/components/EditorHeader/DatabasesSwitcher.jsx @@ -0,0 +1,77 @@ +import { Select } from "@douyinfe/semi-ui"; +import { databases } from "../../data/databases"; +import { DB, State } from "../../data/constants"; +import { useDiagram, useSaveState } from "../../hooks"; +import { db } from "../../data/db"; + +const databasesWithoutGeneric = Object.keys(databases).filter(db => ![DB.GENERIC, DB.MARIADB, DB.MSSQL].includes(databases[db].label)); + +export default function DatabasesSwitcher({ setLastSaved, diagramId, setPrevDatabase }) { + const { database, setDatabase } = useDiagram(); + const { setSaveState } = useSaveState(); + + if (!databases[database] || database === DB.GENERIC) return null; + + const renderOptionItem = renderProps => { + const { + disabled, + selected, + label, + value, + focused, + className, + style, + onMouseEnter, + onClick, + } = renderProps; + const optionCls = [ + 'flex justify-start items-center pl-2 pt-3 cursor-pointer custom-option-render', + focused && 'custom-option-render-focused', + disabled && 'custom-option-render-disabled', + selected && 'custom-option-render-selected', + className, + ].filter(cls => cls).join(' '); + + return ( + <div style={style} className={optionCls} onClick={() => onClick()} onMouseEnter={() => onMouseEnter()}> + {databases[value].image && ( + <img + src={databases[value].image} + className="h-5 pr-2" + style={{ + filter: + "opacity(0.4) drop-shadow(0 0 0 white) drop-shadow(0 0 0 white)", + }} + alt={databases[value].name + " icon"} + title={databases[value].name + " diagram"} + /> + )} + <div className="option-right pr-2">{label}</div> + </div> + ); + }; + const onChangeHandler = async (selectedDb) => { + await db.diagrams + .update(diagramId, { + database: selectedDb, + }).then(() => { + setSaveState(State.SAVED); + setLastSaved(new Date().toLocaleString()); + setPrevDatabase(database); + setDatabase(selectedDb); + }); + }; + + return <Select + className="w-100" + optionList={databasesWithoutGeneric.map((db) => ({ + label: databases[db].name, + value: databases[db].label, + }))} + filter + value={database} + placeholder="Select database" + onChange={onChangeHandler} + renderOptionItem={renderOptionItem} + /> +} \ No newline at end of file diff --git a/src/utils/typesMappings.js b/src/utils/typesMappings.js new file mode 100644 index 000000000..7936967ef --- /dev/null +++ b/src/utils/typesMappings.js @@ -0,0 +1,220 @@ +import { DB } from "../data/constants"; + +function mapDataTypes(fromDb, toDb) { + if (!fromDb || !toDb) { + throw new Error("Please provide both from/to database names."); + } + + fromDb = fromDb.toLowerCase(); + toDb = toDb.toLowerCase(); + + if (fromDb === toDb || [fromDb, toDb].includes(DB.GENERIC)) { + return ""; + } + + const typeMapping = { + postgresql: { + mysql: { + "SMALLINT": "TINYINT", + "INTEGER": "INT", + "BIGINT": "BIGINT", + "SERIAL": "INT AUTO_INCREMENT", + "BIGSERIAL": "BIGINT AUTO_INCREMENT", + "DECIMAL": "DECIMAL", + "NUMERIC": "DECIMAL", + "REAL": "FLOAT", + "DOUBLE PRECISION": "DOUBLE", + "MONEY": "DECIMAL(19,4)", + "VARCHAR": "VARCHAR", + "CHAR": "CHAR", + "TEXT": "TEXT", + "BYTEA": "BLOB", + "BOOLEAN": "TINYINT(1)", + "DATE": "DATE", + "TIME": "TIME", + "TIMETZ": "TIME", + "TIMESTAMP": "DATETIME", + "TIMESTAMPTZ": "DATETIME", + "INTERVAL": "TIME", + "UUID": "CHAR(36)", + "JSON": "JSON", + "JSONB": "JSON", + "XML": "TEXT", + }, + sqlite: { + "SMALLINT": "INTEGER", + "INTEGER": "INTEGER", + "BIGINT": "INTEGER", + "SERIAL": "INTEGER PRIMARY KEY AUTOINCREMENT", + "BIGSERIAL": "INTEGER", + "DECIMAL": "REAL", + "NUMERIC": "REAL", + "REAL": "REAL", + "DOUBLE PRECISION": "REAL", + "MONEY": "REAL", + "VARCHAR": "TEXT", + "CHAR": "TEXT", + "TEXT": "TEXT", + "BYTEA": "BLOB", + "BOOLEAN": "INTEGER", + "DATE": "TEXT", + "TIME": "TEXT", + "TIMETZ": "TEXT", + "TIMESTAMP": "TEXT", + "TIMESTAMPTZ": "TEXT", + "INTERVAL": "TEXT", + "UUID": "TEXT", + "JSON": "TEXT", + "JSONB": "TEXT", + "XML": "TEXT", + }, + }, + mysql: { + postgresql: { + "TINYINT": "SMALLINT", + "SMALLINT": "SMALLINT", + "MEDIUMINT": "INTEGER", + "INT": "INTEGER", + "BIGINT": "BIGINT", + "FLOAT": "REAL", + "DOUBLE": "DOUBLE PRECISION", + "DECIMAL": "DECIMAL", + "NUMERIC": "NUMERIC", + "VARCHAR": "VARCHAR", + "CHAR": "CHAR", + "TEXT": "TEXT", + "BLOB": "BYTEA", + "TINYBLOB": "BYTEA", + "MEDIUMBLOB": "BYTEA", + "LONGBLOB": "BYTEA", + "BIT": "BOOLEAN", + "BOOLEAN": "BOOLEAN", + "DATE": "DATE", + "TIME": "TIME", + "DATETIME": "TIMESTAMP", + "TIMESTAMP": "TIMESTAMP", + "YEAR": "DATE", + "JSON": "JSON", + "ENUM": "VARCHAR", + "SET": "TEXT", + "POINT": "POINT", + "LINESTRING": "LINE", + "POLYGON": "POLYGON" + }, + sqlite: { + "TINYINT": "INTEGER", + "SMALLINT": "INTEGER", + "MEDIUMINT": "INTEGER", + "INT": "INTEGER", + "BIGINT": "INTEGER", + "FLOAT": "REAL", + "DOUBLE": "REAL", + "DECIMAL": "REAL", + "NUMERIC": "REAL", + "VARCHAR(n)": "TEXT", + "CHAR(n)": "TEXT", + "TEXT": "TEXT", + "BLOB": "BLOB", + "TINYBLOB": "BLOB", + "MEDIUMBLOB": "BLOB", + "LONGBLOB": "BLOB", + "BIT": "INTEGER", + "BOOLEAN": "INTEGER", + "DATE": "TEXT", + "TIME": "TEXT", + "DATETIME": "TEXT", + "TIMESTAMP": "TEXT", + "YEAR": "TEXT", + "JSON": "TEXT", + "ENUM": "TEXT", + "SET": "TEXT", + "POINT": "TEXT", + "LINESTRING": "TEXT", + "POLYGON": "TEXT" + }, + }, + sqlite: { + postgresql: { + "INTEGER": "INTEGER", + "REAL": "DOUBLE PRECISION", + "TEXT": "TEXT", + "BLOB": "BYTEA", + "BOOLEAN": "BOOLEAN", + "DATE": "DATE", + "TIME": "TIME", + "DATETIME": "TIMESTAMP", + "NUMERIC": "NUMERIC", + "VARCHAR": "VARCHAR", + "CHAR": "CHAR", + "JSON": "JSON", + "POINT": "POINT", + "LINESTRING": "LINE", + "POLYGON": "POLYGON", + }, + mysql: { + "INTEGER": "INTEGER", + "REAL": "DOUBLE", + "TEXT": "TEXT", + "BLOB": "BLOB", + "BOOLEAN": "TINYINT(1)", + "DATE": "DATE", + "TIME": "TIME", + "DATETIME": "DATETIME", + "NUMERIC": "DECIMAL", + "VARCHAR": "VARCHAR", + "CHAR": "CHAR", + "JSON": "JSON", + "POINT": "POINT", + "LINESTRING": "LINESTRING", + "POLYGON": "POLYGON", + }, + }, + }; + + if (typeMapping[fromDb] && typeMapping[fromDb][toDb]) { + return typeMapping[fromDb][toDb]; + } else { + return ''; // Unsupported data type mapping + } +} + +export function convertTableSchema(table, fromDb, toDb) { + + return { + ...table, + fields: table.fields.map(field => { + let mappedType = mapDataTypes(fromDb, toDb)?.[field.type]; + + // Handle ENUM values if they exist + if (field.type === "ENUM" && [DB.POSTGRES, DB.SQLITE].includes(toDb)) { + mappedType = toDb === DB.POSTGRES ? "VARCHAR" : "TEXT"; + + if (field.values?.length) { + field.check = `${field.name} IN (${field.values.join(", ")})`; + } + } + + if (field.type === "VARCHAR" && field.check && DB.MYSQL === toDb) { + mappedType = `ENUM`; + const regex = /\(([^)]+)\)/; + const match = field.check.match(regex); + + if (match) { + field.values = match[1].split(',').map(value => value.trim()); + mappedType += `(${field.values.join(", ")})`; + } + } + + if (DB.POSTGRES === toDb && field.increment) { + mappedType = `SERIAL`; + } + + return { + ...field, + type: mappedType || field.type, + // Remove MySQL-specific properties if necessary + values: field.values || undefined + }; + }) + }; +}