From 2290c7245142e31c76dbfde4b14b8dfe282c85f3 Mon Sep 17 00:00:00 2001 From: Sokphal Adam <31284849+sokphaladam@users.noreply.github.com> Date: Thu, 19 Dec 2024 08:49:36 +0700 Subject: [PATCH] Schema database (#210) * todo: schema/database * prevent postgres * database schema * change: schema database * change: create schema database from tab to use dialog * update: schema database --- src/components/gui/schema-editor/index.tsx | 20 +-- .../gui/schema-editor/schema-create/index.tsx | 21 +++ .../schema-create/schema-create-form.tsx | 144 ++++++++++++++++++ .../schema-database-collation.tsx | 37 +++++ src/components/gui/schema-sidebar-list.tsx | 77 ++++++---- src/components/gui/schema-sidebar.tsx | 73 +++++++-- src/drivers/base-driver.ts | 11 ++ src/drivers/mysql/generate-schema.ts | 19 +++ src/drivers/mysql/mysql-driver.ts | 11 +- src/drivers/postgres/postgres-driver.ts | 5 + src/drivers/sqlite-base-driver.ts | 5 + src/messages/open-tab.tsx | 1 - 12 files changed, 366 insertions(+), 58 deletions(-) create mode 100644 src/components/gui/schema-editor/schema-create/index.tsx create mode 100644 src/components/gui/schema-editor/schema-create/schema-create-form.tsx create mode 100644 src/components/gui/schema-editor/schema-create/schema-database-collation.tsx diff --git a/src/components/gui/schema-editor/index.tsx b/src/components/gui/schema-editor/index.tsx index 521e76f..88e4f3c 100644 --- a/src/components/gui/schema-editor/index.tsx +++ b/src/components/gui/schema-editor/index.tsx @@ -34,17 +34,17 @@ export default function SchemaEditor({ const newColumn = value.columns.length === 0 ? { - name: "id", - type: databaseDriver.columnTypeSelector.idTypeName ?? "INTEGER", - constraint: { - primaryKey: true, - }, - } + name: "id", + type: databaseDriver.columnTypeSelector.idTypeName ?? "INTEGER", + constraint: { + primaryKey: true, + }, + } : { - name: "column", - type: databaseDriver.columnTypeSelector.textTypeName ?? "TEXT", - constraint: {}, - }; + name: "column", + type: databaseDriver.columnTypeSelector.textTypeName ?? "TEXT", + constraint: {}, + }; onChange({ ...value, diff --git a/src/components/gui/schema-editor/schema-create/index.tsx b/src/components/gui/schema-editor/schema-create/index.tsx new file mode 100644 index 0000000..4c84c25 --- /dev/null +++ b/src/components/gui/schema-editor/schema-create/index.tsx @@ -0,0 +1,21 @@ +import { Dialog, DialogContent, DialogHeader } from "@/components/ui/dialog"; +import React from "react"; +import { SchemaDatabaseCreateForm } from "./schema-create-form"; + +interface Props { + schemaName?: string; + onClose: () => void +} + +export default function SchemaCreateDialog(props: React.PropsWithChildren) { + return ( + + + + {!props.schemaName ? 'New Schema/Database' : props.schemaName + "-Schema"} + + + + + ) +} \ No newline at end of file diff --git a/src/components/gui/schema-editor/schema-create/schema-create-form.tsx b/src/components/gui/schema-editor/schema-create/schema-create-form.tsx new file mode 100644 index 0000000..03f4a5d --- /dev/null +++ b/src/components/gui/schema-editor/schema-create/schema-create-form.tsx @@ -0,0 +1,144 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useSchema } from "@/context/schema-provider"; +import { LucideAlertCircle, LucideLoader, LucideSave } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useDatabaseDriver } from "@/context/driver-provider"; +import { DatabaseSchemaChange } from "@/drivers/base-driver"; +import { SchemaDatabaseCollation } from "./schema-database-collation"; + +export function SchemaDatabaseCreateForm({ schemaName, onClose }: { schemaName?: string; onClose: () => void; }) { + const { databaseDriver } = useDatabaseDriver(); + const { schema, refresh: refreshSchema } = useSchema(); + const [loading, setLoading] = useState(false); + const [currentCollate, setCurrentCollate] = useState(''); + const [isExecuting, setIsExecuting] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [value, setValue] = useState({ + name: { + new: '', + old: '' + }, + createScript: '', + collate: '' + }); + + const previewScript = useMemo(() => { + return databaseDriver.createUpdateDatabaseSchema(value).join(";\n"); + }, [databaseDriver, value]); + + const fetchData = useCallback(async () => { + setLoading(true); + + try { + const { rows } = await databaseDriver.query(` + SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${schemaName}'; + `) + + if (rows.length > 0) { + setValue({ + ...value, + name: { + new: schemaName, + old: schemaName + }, + collate: String(rows[0].DEFAULT_COLLATION_NAME) + }) + setCurrentCollate(String(rows[0].DEFAULT_COLLATION_NAME)) + } + } catch (error) { + // + } finally { + setLoading(false); + } + }, [databaseDriver, schemaName, value]) + + useEffect(() => { + if (schemaName) { + fetchData().then().catch(console.error); + } + }, []) + + // const toggleSave = useCallback(() => setSaving(!isSaving), [isSaving]) + + const onSave = useCallback(() => { + { + setIsExecuting(true); + databaseDriver.transaction([previewScript]).then(() => { + refreshSchema(); + onClose(); + }).catch((err) => setErrorMessage((err as Error).message)).finally(() => { + setIsExecuting(false); + }) + } + }, [databaseDriver, onClose, previewScript, refreshSchema]) + + const schemaNames = Object.keys(schema).filter(s => s !== schemaName).map(s => s); + const schemaNameExists = schemaNames.includes(value.name.new || ''); + const isChange = value.name.new !== value.name.old || currentCollate !== value.collate + + return ( +
+ {errorMessage && ( +
+ +

{errorMessage}

+
+ )} +
+
+
Schema Name
+ { + setValue({ + ...value, + name: { + ...value.name, + new: e.currentTarget.value + } + }) + }} + disabled={loading || !!schemaName} + className={`w-full ${schemaNameExists ? 'border-red-600' : ''}`} + /> + { + schemaNameExists && The schema name `{value.name.new}` already exists. + } +
+
+
Collation
+ { + setValue({ + ...value, + collate: selectedSchema + }) + }} + /> +
+
+
+
+
+ +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/gui/schema-editor/schema-create/schema-database-collation.tsx b/src/components/gui/schema-editor/schema-create/schema-database-collation.tsx new file mode 100644 index 0000000..efa9d73 --- /dev/null +++ b/src/components/gui/schema-editor/schema-create/schema-database-collation.tsx @@ -0,0 +1,37 @@ +import { useDatabaseDriver } from "@/context/driver-provider"; +import { Input } from "@/components/ui/input"; + +interface SchemaCollateSelectProps { + value?: string; + onChange: (value: string) => void; + readonly?: boolean; +} + +export function SchemaDatabaseCollation( + { + onChange, + value, + }: SchemaCollateSelectProps +) { + const driver = useDatabaseDriver(); + + return ( + <> + onChange(e.currentTarget.value)} + /> + {driver.databaseDriver.getCollationList().length > 0 && ( + + {driver.databaseDriver.getCollationList().map((collation) => ( + + )} + + ) +} \ No newline at end of file diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index 5322016..01d74bb 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -7,6 +7,7 @@ import { useSchema } from "@/context/schema-provider"; import { ListView, ListViewItem } from "../listview"; import { useDatabaseDriver } from "@/context/driver-provider"; import { Table } from "@phosphor-icons/react"; +import SchemaCreateDialog from "./schema-editor/schema-create"; interface SchemaListProps { search: string; @@ -104,6 +105,7 @@ export default function SchemaList({ search }: Readonly) { const { databaseDriver } = useDatabaseDriver(); const [selected, setSelected] = useState(""); const { refresh, schema, currentSchemaName } = useSchema(); + const [editSchema, setEditSchema] = useState(null); const [collapsed, setCollapsed] = useState(() => { return new Set(); @@ -119,6 +121,12 @@ export default function SchemaList({ search }: Readonly) { const isTable = item?.type === "table"; return [ + item?.type === 'schema' && { + title: 'Edit', + onClick: () => { + setEditSchema(item.schemaName); + } + }, { title: "Copy Name", disabled: !selectedName, @@ -190,39 +198,42 @@ export default function SchemaList({ search }: Readonly) { ); return ( - prepareContextMenu(item?.data)} - selectedKey={selected} - onSelectChange={setSelected} - onDoubleClick={(item) => { - if (item.data.type === "table" || item.data.type === "view") { - openTab({ - type: "table", - schemaName: item.data.schemaName ?? "", - tableName: item.data.name, - }); - } else if (item.data.type === "trigger") { - openTab({ - type: "trigger", - schemaName: item.data.schemaName, - name: item.name, - }); - } else if (item.data.type === "schema") { - if (databaseDriver.getFlags().supportUseStatement) { - databaseDriver - .query("USE " + databaseDriver.escapeId(item.name)) - .then(() => { - refresh(); - }); + <> + {editSchema && setEditSchema(null)} />} + prepareContextMenu(item?.data)} + selectedKey={selected} + onSelectChange={setSelected} + onDoubleClick={(item) => { + if (item.data.type === "table" || item.data.type === "view") { + openTab({ + type: "table", + schemaName: item.data.schemaName ?? "", + tableName: item.data.name, + }); + } else if (item.data.type === "trigger") { + openTab({ + type: "trigger", + schemaName: item.data.schemaName, + name: item.name, + }); + } else if (item.data.type === "schema") { + if (databaseDriver.getFlags().supportUseStatement) { + databaseDriver + .query("USE " + databaseDriver.escapeId(item.name)) + .then(() => { + refresh(); + }); + } } - } - }} - /> + }} + /> + ); } diff --git a/src/components/gui/schema-sidebar.tsx b/src/components/gui/schema-sidebar.tsx index 8576083..e13bf27 100644 --- a/src/components/gui/schema-sidebar.tsx +++ b/src/components/gui/schema-sidebar.tsx @@ -7,11 +7,15 @@ import { cn } from "@/lib/utils"; import { buttonVariants } from "../ui/button"; import { Plus } from "@phosphor-icons/react"; import { useDatabaseDriver } from "@/context/driver-provider"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { Command, CommandItem, CommandList } from "../ui/command"; +import SchemaCreateDialog from "./schema-editor/schema-create"; export default function SchemaView() { const [search, setSearch] = useState(""); const { databaseDriver } = useDatabaseDriver(); const { currentSchemaName } = useSchema(); + const [isCreateSchema, setIsCreateSchema] = useState(false); const onNewTable = useCallback(() => { openTab({ @@ -20,24 +24,67 @@ export default function SchemaView() { }); }, [currentSchemaName]); + const toggleNewDate = useCallback(() => setIsCreateSchema(!isCreateSchema), [isCreateSchema]) + + const ActivatorButton = () => { + + if (!databaseDriver.getFlags().supportCreateUpdateDatabase && !databaseDriver.getFlags().supportCreateUpdateTable) { + return <> + } + + if (databaseDriver.getFlags().dialect === 'sqlite' && databaseDriver.getFlags().supportCreateUpdateTable) { + return ( + + ) + } + + return ( + + + + + + + + { + databaseDriver.getFlags().supportCreateUpdateDatabase && New schema/database + } + { + databaseDriver.getFlags().supportCreateUpdateTable && onNewTable()}>New table + } + + + + + ) + } + return (
+ {isCreateSchema && }

Tables

- {databaseDriver.getFlags().supportCreateUpdateTable && ( - - )} +
diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index 0184684..b7cdfa8 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -240,6 +240,7 @@ export interface DriverFlags { supportInsertReturning: boolean; supportUpdateReturning: boolean; supportRowId: boolean; + supportCreateUpdateDatabase: boolean; } export interface DatabaseTableColumnChange { @@ -265,6 +266,15 @@ export interface DatabaseTableSchemaChange { createScript?: string; } +export interface DatabaseSchemaChange { + name: { + old?: string; + new?: string; + }; + createScript?: string; + collate?: string; +} + export abstract class BaseDriver { // Flags abstract getFlags(): DriverFlags; @@ -323,4 +333,5 @@ export abstract class BaseDriver { abstract emptyTable(schemaName: string, tableName: string): Promise; abstract createUpdateTableSchema(change: DatabaseTableSchemaChange): string[]; + abstract createUpdateDatabaseSchema(change: DatabaseSchemaChange): string[]; } diff --git a/src/drivers/mysql/generate-schema.ts b/src/drivers/mysql/generate-schema.ts index 492fc1f..14becbc 100644 --- a/src/drivers/mysql/generate-schema.ts +++ b/src/drivers/mysql/generate-schema.ts @@ -1,5 +1,6 @@ import { BaseDriver, + DatabaseSchemaChange, DatabaseTableColumn, DatabaseTableColumnConstraint, DatabaseTableSchemaChange, @@ -127,6 +128,24 @@ function generateConstraintScript( } } +export function generateMysqlDatabaseSchema( + driver: BaseDriver, + change: DatabaseSchemaChange +): string[] { + const isCreateScript = !change.name.old; + let line = ""; + + if (change.collate) { + line = ` COLLATE \`${change.collate}\``; + } + + if (isCreateScript) { + return [`CREATE DATABASE \`${change.name.new}\`${line}`]; + } else { + return [`ALTER DATABASE \`${change.name.old}\`${line}`]; + } +} + // https://dev.mysql.com/doc/refman/8.4/en/create-table.html export function generateMySqlSchemaChange( driver: BaseDriver, diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index 92f5ba2..6908403 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -9,10 +9,14 @@ import { DatabaseTableSchemaChange, ColumnTypeSelector, DatabaseTableColumnConstraint, + DatabaseSchemaChange, } from "../base-driver"; import CommonSQLImplement from "../common-sql-imp"; import { escapeSqlValue } from "../sqlite/sql-helper"; -import { generateMySqlSchemaChange } from "./generate-schema"; +import { + generateMysqlDatabaseSchema, + generateMySqlSchemaChange, +} from "./generate-schema"; import { MYSQL_COLLATION_LIST, MYSQL_DATA_TYPE_SUGGESTION, @@ -124,6 +128,7 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { supportModifyColumn: true, mismatchDetection: false, supportCreateUpdateTable: true, + supportCreateUpdateDatabase: true, dialect: "mysql", supportUseStatement: true, @@ -374,6 +379,10 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { return generateMySqlSchemaChange(this, change); } + createUpdateDatabaseSchema(change: DatabaseSchemaChange): string[] { + return generateMysqlDatabaseSchema(this, change); + } + inferTypeFromHeader(): TableColumnDataType | undefined { return undefined; } diff --git a/src/drivers/postgres/postgres-driver.ts b/src/drivers/postgres/postgres-driver.ts index cf9e3bd..417c73d 100644 --- a/src/drivers/postgres/postgres-driver.ts +++ b/src/drivers/postgres/postgres-driver.ts @@ -77,6 +77,7 @@ export default abstract class PostgresLikeDriver extends CommonSQLImplement { supportModifyColumn: false, mismatchDetection: false, supportCreateUpdateTable: false, + supportCreateUpdateDatabase: false, supportInsertReturning: true, supportUpdateReturning: true, }; @@ -350,6 +351,10 @@ WHERE throw new Error("Not implemented"); } + createUpdateDatabaseSchema(): string[] { + throw new Error("Not implemented"); + } + inferTypeFromHeader(): TableColumnDataType | undefined { return undefined; } diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index e6be8ad..6895c44 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -51,6 +51,7 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { optionalSchema: true, mismatchDetection: false, supportCreateUpdateTable: true, + supportCreateUpdateDatabase: false, dialect: "sqlite", }; } @@ -230,6 +231,10 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { return generateSqlSchemaChange(change); } + createUpdateDatabaseSchema(): string[] { + throw new Error("Not implemented"); + } + override async findFirst( schemaName: string, tableName: string, diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index 42e293e..65d8287 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -148,7 +148,6 @@ function generateComponent(tab: OpenTabsProps, title: string) { if (tab.type === "mass-drop-table") return ; if (tab.type === "trigger") return ; - return
Unknown Tab
; }