render mysql code

This commit is contained in:
KarimTamani
2025-06-08 21:27:42 +01:00
parent 2c7765350d
commit 071bd5eecc
15 changed files with 273 additions and 110 deletions
+57 -8
View File
@@ -1,5 +1,5 @@
import { Autocomplete as HeroUiAutocomplete, AutocompleteItem } from "@heroui/react";
import { Autocomplete as HeroUiAutocomplete, AutocompleteItem, AutocompleteSection } from "@heroui/react";
import { Key } from "react";
interface AutocompleteProps {
@@ -10,29 +10,60 @@ interface AutocompleteProps {
onSelectionChange?: (item: any) => void,
placeholder?: string
isDisabled?: boolean
selectedItem?: Key
selectedItem?: Key,
grouped?: boolean
}
const headingClasses =
"flex w-full py-1.5 px-2 bg-default shadow-small rounded-small";
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem }) => {
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem, grouped = false }) => {
const onItemChange = (item: Key | null) => {
onSelectionChange && onSelectionChange(item);
}
if (!grouped)
return (
<HeroUiAutocomplete
radius="sm"
isDisabled={isDisabled}
className="w-full"
defaultItems={items || []}
size="sm"
onSelectionChange={onItemChange}
defaultSelectedKey={defaultSelection as any}
variant="bordered"
aria-label={placeholder}
placeholder={placeholder}
selectedKey={selectedItem as any}
inputProps={{
classNames: {
inputWrapper: "border-divider group-hover:border-primary",
return (
<HeroUiAutocomplete
},
}}
>
{(item: any) => <AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>}
</HeroUiAutocomplete>
)
if (grouped) {
return <HeroUiAutocomplete
radius="sm"
isDisabled={isDisabled}
className="w-full"
defaultItems={items || []}
size="sm"
onSelectionChange={onItemChange}
defaultSelectedKey={defaultSelection as any}
variant="bordered"
aria-label={placeholder}
placeholder={placeholder}
selectedKey={selectedItem as any}
inputProps={{
classNames: {
@@ -43,9 +74,27 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
}}
>
{(item: any) => <AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>}
{
items ? Object.keys(items).map((key: any) => (
<AutocompleteSection
classNames={{
heading: headingClasses,
}}
title={key}
>
{
items[key].map((item: any) => (
<AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>
))
}
</AutocompleteSection>
)) : []
}
</HeroUiAutocomplete>
)
}
}
export default Autocomplete;
+6 -2
View File
@@ -7,6 +7,7 @@ import { useEffect, useState } from "react";
import { Parser } from "node-sql-parser";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { format } from 'sql-formatter';
import { getDatabaseByDialect } from "@/lib/database";
const parser = new Parser();
@@ -16,8 +17,11 @@ export const useRenderSql = (database: DatabaseType) => {
const { data_types } = useDatabaseOperations();
useEffect(() => {
const dbAst: any = DatabaseToAst(database, data_types);
const formattedSqlCode = format(parser.sqlify(dbAst), { language: 'sql' }); // or 'mysql', 'postgresql', etc.
const dbAst: any = DatabaseToAst(database, data_types);
const formattedSqlCode = format(parser.sqlify(dbAst , {
database : "MySQL" // getDatabaseByDialect(database.dialect).name
}), { language: 'sql' });
setSql(
formattedSqlCode
);
+2 -2
View File
@@ -10,11 +10,11 @@ export interface DatabaseType {
export const DBTypes: DatabaseType[] = [
{
name: "PostgreSql",
name: "Postgresql",
dialect: "postgres",
logo: "/postgresql_logo.png"
}, {
name: "Mysql",
name: "MySQL",
dialect: "mysql",
logo: "/mysql_logo.png"
},
+1
View File
@@ -9,6 +9,7 @@ export const data_types = sqliteTable('data_types', {
dialect: text("dialect", {
enum: ["postgres", "mysql", "sqlite", "mariadb"],
}).notNull().default("postgres"),
type: text('type').notNull()
});
+17 -11
View File
@@ -20,7 +20,7 @@ import { TableInsertType } from "@/lib/schemas/table-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useTableToNode } from "@/hooks/use-table-to-node";
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema";
// Utils and constants
import { v4 } from "uuid";
@@ -38,6 +38,8 @@ import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
import { useTheme } from "next-themes";
import { Parser } from "node-sql-parser";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
const parser = new Parser();
const DatabasePage: React.FC = () => {
@@ -65,25 +67,29 @@ const DatabasePage: React.FC = () => {
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
// Called when a connection is made between fields
const onConnect = useCallback((connection: Connection) => {
const sourceFieldId: string | undefined = (connection.sourceHandle as string).split("_").pop();
const targetFieldId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, "");
const onConnect = useCallback(async (connection: Connection) => {
const sourceId: string | undefined = (connection.sourceHandle as string).split("_").pop();
const targetId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, "");
const sourceField: FieldType | undefined = getField(connection.source, sourceFieldId as string);
const targetField: FieldType | undefined = getField(connection.target, targetFieldId);
const sourceField: FieldType = getField(connection.source, sourceId as string) as FieldType;
const targetField: FieldType = getField(connection.target, targetId) as FieldType;
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
// Check if both fields have the same type (valid relationship)
if (sourceField?.typeId == targetField?.typeId) {
createRelationship({
await createRelationship({
id: v4(),
sourceTableId: connection.source,
targetTableId: connection.target,
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId
targetFieldId,
} as RelationshipInsertType);
// Add edge to the diagram
setEdges((eds) => addEdge(connection, eds));
//setEdges((eds) => addEdge(connection, eds));
} else {
// Show error toast if invalid relationship
addToast({
@@ -2,34 +2,33 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Button, Select, SelectItem, SharedSelection } from "@heroui/react";
import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
interface RelationshipAccordionBodyProps {
relationship: RelationshipType
}
const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ relationship }) => {
const [cardinality, setCardinality] = useState(new Set([relationship.cardinality]));
const { editRelationship, deleteRelationship } = useDatabaseOperations();
const { t } = useTranslation();
const changeCardinality = (keys: SharedSelection) => {
if (keys.anchorKey != relationship.cardinality)
if (keys.anchorKey != relationship.cardinality) {
editRelationship({
id: relationship.id,
cardinality: keys.anchorKey as Cardinality
cardinality: keys.anchorKey as Cardinality,
} as RelationshipInsertType);
}
setCardinality(keys as any);
}
@@ -11,23 +11,27 @@ import { Parser } from "node-sql-parser";
const parser = new Parser();
const code = `
-- Addresses table with a foreign key to users
CREATE TABLE addresses (
id INTEGER PRIMARY KEY,
user_id INT NOT NULL,
street VARCHAR(255),
city VARCHAR(100),
state VARCHAR(100),
postal_code VARCHAR(20),
country VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
CREATE TABLE \`users\` (
id BIGINT NOT NULL PRIMARY KEY UNIQUE,
name VARCHAR(255) NOT NULL,
lastname VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
profile_url VARCHAR(255),
age SMALLINT,
birthday DATE,
created_at TIMESTAMP NOT NULL
);
-- Indexes to improve lookup performance
CREATE UNIQUE INDEX idx_addresses_user_id ON addresses(user_id);
CREATE INDEX idx_addresses_city ON addresses(city , country);
CREATE TABLE \`addresses\` (
id BIGINT NOT NULL PRIMARY KEY UNIQUE,
street_line VARCHAR(255) NOT NULL,
latitude FLOAT,
longitude FLOAT, -- fixed spelling
state_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
FOREIGN KEY (\`user_id\`) REFERENCES \`users\` (\`id\`)
);
`
@@ -40,7 +44,9 @@ const SqlPreview: React.FC = ({ }) => {
const {resolvedTheme} = useTheme();
useEffect(() => {
const ast = parser.astify(code )
const ast = parser.astify(code , {
database : "Mysql"
} )
console.log (ast)
}, [])
@@ -21,7 +21,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
const [fieldName, setFieldName] = useState<string>(field.name);
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const { data_types } = useDatabaseOperations();
const { data_types , grouped_data_types } = useDatabaseOperations();
const { deleteField, editField } = useDatabaseOperations();
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
@@ -109,9 +109,9 @@ const FieldItem: React.FC<Props> = ({ field }) => {
}}
/>
<Autocomplete
items={data_types}
items={grouped_data_types}
onSelectionChange={updateFieldType}
grouped
selectedItem={selectedType}
placeholder={t("db_controller.type")}
/>
@@ -12,9 +12,6 @@ import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useReactFlow } from "@xyflow/react";
import SqlPreview from "../sql-preview";
interface Props { }
const PADDING_X = 40;
const PADDING_Y = 80;
@@ -27,6 +27,7 @@ interface DatabaseDataContextType {
interface DatabaseOperationsContextType {
data_types: DataType[],
grouped_data_types : any ;
// database operations
createDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
editDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
@@ -11,7 +11,7 @@ import { RelationshipInsertType, relationships } from "@/lib/schemas/relationshi
import DatabaseHistoryProvider from "../database-history/database-history-provider";
import { DBDiffOperation } from "@/utils/database";
import { DatabaseInsertType, DatabaseType, databases as databaseModel } from "@/lib/schemas/database-schema";
import { getTimestamp } from "@/utils/utils";
import { getTimestamp, groupBy } from "@/utils/utils";
import { IndexInsertType, indices } from "@/lib/schemas/index-schema";
import { field_indices } from "@/lib/schemas/field_index-schema";
import { v4 } from "uuid";
@@ -87,6 +87,12 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
where : (data_types , { eq })=> eq(data_types.dialect, (database as any).dialect)
})
));
const grouped_data_types : any = useMemo(() => {
return groupBy(data_types , "type") ;
} , [data_types])
// Auto-select first database if none is selected
useEffect(() => {
@@ -380,7 +386,8 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editIndex,
deleteIndex,
editFieldIndices,
data_types
data_types ,
grouped_data_types
}), [
createDatabase,
editDatabase,
@@ -403,7 +410,8 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editIndex,
deleteIndex,
editFieldIndices,
data_types
data_types ,
grouped_data_types
]);
return (
+6 -6
View File
@@ -1,6 +1,6 @@
import { FieldType } from "@/lib/schemas/field-schema";
import { v4 } from "uuid";
@@ -17,13 +17,13 @@ export const getNextSequence = (fields: FieldType[]): number => {
export const cloneField = ( field : FieldType) : FieldType => {
export const cloneField = (field: FieldType): FieldType => {
return {
...field ,
id : v4() ,
} as FieldType ;
...field,
id: v4(),
} as FieldType;
}
+50
View File
@@ -0,0 +1,50 @@
import { FieldType } from "@/lib/schemas/field-schema"
export const getRelationshipSourceAndTarget = (sourceTableId: string, sourceField: FieldType, targetTableId: string, targetField: FieldType) => {
if (sourceField.isPrimary || targetField.isPrimary) {
if (sourceField.isPrimary) {
return {
sourceTableId: sourceTableId,
targetTableId: targetTableId,
sourceFieldId: sourceField.id,
targetFieldId: targetField.id,
}
} else {
return {
sourceTableId: targetTableId,
targetTableId: sourceTableId,
sourceFieldId: targetField.id,
targetFieldId: sourceField.id,
}
}
}
else if (sourceField.unique || targetField.unique) {
if (sourceField.unique) {
return {
sourceTableId: sourceTableId,
targetTableId: targetTableId,
sourceFieldId: sourceField.id,
targetFieldId: targetField.id,
}
} else {
return {
sourceTableId: targetTableId,
targetTableId: sourceTableId,
sourceFieldId: targetField.id,
targetFieldId: sourceField.id,
}
}
} else {
return {
sourceTableId: sourceTableId,
targetTableId: targetTableId,
sourceFieldId: sourceField.id,
targetFieldId: targetField.id,
}
}
}
+76 -46
View File
@@ -5,7 +5,7 @@ import { DatabaseType } from "@/lib/schemas/database-schema";
import { FieldType } from "@/lib/schemas/field-schema";
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
import { IndexType } from "@/lib/schemas/index-schema";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
import { TableType } from "@/lib/schemas/table-schema";
@@ -16,8 +16,6 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) =>
for (const table of database.tables) {
dbAst.push(TableToAst({
...table,
sourceRelations: database.relationships.filter((relationship: RelationshipType) => relationship.sourceTableId == table.id),
targetRelations: database.relationships.filter((relationship: RelationshipType) => relationship.targetTableId == table.id),
}, data_types));
if (table.indices && table.indices.length > 0)
@@ -28,55 +26,37 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) =>
}, table));
};
for (const relationship of database.relationships) {
dbAst.push(relationshipToAst(relationship))
}
//console.log (dbAst)
return dbAst;
}
export const TableToAst = (table: TableType, data_types: DataType[]) => {
const constraints: any[] = table.sourceRelations ? table.sourceRelations?.map((relationship: RelationshipType) => {
return {
constraint_type: "FOREIGN KEY",
resource: "constraint",
definition: [
{
type: "column_ref",
column: relationship.sourceField.name,
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
const multiPrimaryKeys: boolean = primaryKeys.length > 1;
const primaryKeysConstraints: any[] | undefined = multiPrimaryKeys ? [{
constraint_type: "primary key",
resource: "constraint",
definition: primaryKeys.map((field: FieldType) => ({
column: field.name,
type: "column_ref"
}))
}] : [];
}
],
reference_definition: {
on_action: [
{
"type": "on delete",
"value": {
"type": "origin",
"value": "cascade"
}
}
],
keyword: "references",
table: [{
table: relationship.targetTable.name,
}],
definition: [
{
type: "column_ref",
column: relationship.targetField.name,
}
],
}
}
}) : [];
const constraints: any[] = [...primaryKeysConstraints];
const filed_definitions = table.fields.map((field: FieldType) => FieldToAst({
...field,
type: data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType,
sourceRelations: table.sourceRelations?.filter((relationship: RelationshipType) => relationship.sourceFieldId == field.id),
targetRelations: table.targetRelations?.filter((relationship: RelationshipType) => relationship.targetFieldId == field.id),
}));
}, multiPrimaryKeys));
return {
keyword: "table",
type: "create",
@@ -85,12 +65,8 @@ export const TableToAst = (table: TableType, data_types: DataType[]) => {
}],
create_definitions: [...filed_definitions, ...constraints]
}
}
export const FieldToAst = (field: FieldType) => {
export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false) => {
return {
column: {
type: "column_ref",
@@ -108,9 +84,11 @@ export const FieldToAst = (field: FieldType) => {
},
definition: {
dataType: field.type?.name?.toLocaleUpperCase(),
length : field.type.name == "varchar" && field.type.dialect == "mysql" ? 255 : null ,
},
primary_key: field.isPrimary ? "primary key" : null,
primary_key: field.isPrimary && !ignorePrimaryKey ? "primary key" : null,
resource: "column"
}
}
@@ -124,10 +102,62 @@ export const IndexToAst = (index: IndexType, table: TableType) => {
type: "create",
table: { table: table.name },
keyword: "index",
on_kw: "on",
index_type: index.unique ? "unique" : null,
index_columns: index.fields.map((field: FieldType) => ({
column: field.name,
type: "column_ref"
}))
}
}
export const relationshipToAst = (relationship: RelationshipType) => {
console.log(relationship);
let primaryKey: FieldType = relationship.sourceField;
let foreignKey: FieldType = relationship.targetField;
let sourceTable : TableType = relationship.sourceTable ;
let targetTable : TableType = relationship.targetTable ;
if (relationship.cardinality == Cardinality.many_to_one) {
primaryKey = relationship.targetField;
foreignKey = relationship.sourceField;
sourceTable = relationship.targetTable ;
targetTable = relationship.sourceTable ;
}
return {
table: [{
table: targetTable.name,
}],
type: "alter",
expr: [{
action: "add",
resource: "constraint",
type: "alter",
create_definitions: {
constraint: relationship.name ? relationship.name : null,
constraint_type: "FOREIGN KEY",
index: null,
keyword: null,
resource: "constraint",
definition: [{
column: foreignKey.name,
type: "column_ref"
}],
reference_definition: {
keyword: "references",
on_action: [],
table: [{ table: sourceTable.name }],
definition: [{
column: primaryKey.name,
type: "column_ref"
}],
}
}
}]
}
}
+15 -3
View File
@@ -52,12 +52,24 @@ function excludeFields(
}
function groupBy(array: any[], key: string) {
return array.reduce((acc, item) => {
const groupKey = item[key];
if (!acc[groupKey]) {
acc[groupKey] = [];
}
acc[groupKey].push(item);
return acc;
}, {});
}
export {
areArraysEqual,
getTimestamp,
excludeFields ,
excludeFields,
groupBy
}