From b81363d2a7d053140bcaccca652977feff595523 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 16 Aug 2024 10:07:58 +0700 Subject: [PATCH 01/17] trying to rework driver --- src/drivers/common-sql-imp.ts | 112 ++++++++++++++++++++++++++++ src/drivers/iframe-driver.ts | 14 ++-- src/drivers/mysql/mysql-driver.ts | 5 ++ src/drivers/sqlite-base-driver.ts | 117 +----------------------------- 4 files changed, 127 insertions(+), 121 deletions(-) create mode 100644 src/drivers/common-sql-imp.ts create mode 100644 src/drivers/mysql/mysql-driver.ts diff --git a/src/drivers/common-sql-imp.ts b/src/drivers/common-sql-imp.ts new file mode 100644 index 0000000..a3a7675 --- /dev/null +++ b/src/drivers/common-sql-imp.ts @@ -0,0 +1,112 @@ +import { validateOperation } from "@/components/lib/validation"; +import { + BaseDriver, + DatabaseTableOperation, + DatabaseTableOperationReslt, + DatabaseTableSchema, +} from "./base-driver"; +import { + generateDeleteStatement, + generateInsertStatement, + generateSelectOneWithConditionStatement, + generateUpdateStatement, +} from "./sqlite/sql-helper"; + +export default abstract class CommonSQLImplement extends BaseDriver { + protected validateUpdateOperation( + ops: DatabaseTableOperation[], + validateSchema: DatabaseTableSchema + ) { + for (const op of ops) { + const { valid, reason } = validateOperation(op, validateSchema); + if (!valid) { + throw new Error(reason); + } + } + } + + async updateTableData( + tableName: string, + ops: DatabaseTableOperation[], + validateSchema?: DatabaseTableSchema + ): Promise { + if (validateSchema) { + this.validateUpdateOperation(ops, validateSchema); + } + + const sqls = ops.map((op) => { + if (op.operation === "INSERT") + return generateInsertStatement(tableName, op.values); + if (op.operation === "DELETE") + return generateDeleteStatement(tableName, op.where); + + return generateUpdateStatement(tableName, op.where, op.values); + }); + + const result = await this.transaction(sqls); + + const tmp: DatabaseTableOperationReslt[] = []; + + for (let i = 0; i < result.length; i++) { + const r = result[i]; + const op = ops[i]; + + if (!r || !op) { + tmp.push({}); + continue; + } + + if (op.operation === "UPDATE") { + const selectStatement = generateSelectOneWithConditionStatement( + tableName, + op.where + ); + + // This transform to make it friendly for sending via HTTP + const selectResult = await this.query(selectStatement); + + tmp.push({ + lastId: r.lastInsertRowid, + record: selectResult.rows[0], + }); + } else if (op.operation === "INSERT") { + if (op.autoIncrementPkColumn) { + const selectStatement = generateSelectOneWithConditionStatement( + tableName, + { [op.autoIncrementPkColumn]: r.lastInsertRowid } + ); + + // This transform to make it friendly for sending via HTTP + const selectResult = await this.query(selectStatement); + + tmp.push({ + record: selectResult.rows[0], + lastId: r.lastInsertRowid, + }); + } else if (op.pk && op.pk.length > 0) { + const selectStatement = generateSelectOneWithConditionStatement( + tableName, + op.pk.reduce>((a, b) => { + a[b] = op.values[b]; + return a; + }, {}) + ); + + // This transform to make it friendly for sending via HTTP + const selectResult = await this.query(selectStatement); + + tmp.push({ + record: selectResult.rows[0], + lastId: r.lastInsertRowid, + }); + } else { + tmp.push({}); + } + } else { + tmp.push({}); + } + } + + return tmp; + } +} diff --git a/src/drivers/iframe-driver.ts b/src/drivers/iframe-driver.ts index 993cfba..6906268 100644 --- a/src/drivers/iframe-driver.ts +++ b/src/drivers/iframe-driver.ts @@ -20,13 +20,10 @@ type PromiseResolveReject = { reject: (value: string) => void; }; -export default class IframeDriver extends SqliteLikeBaseDriver { +class IframeConnection { protected counter = 0; protected queryPromise: Record = {}; - /** - * This will listen to the parent window response - */ listen() { const handler = (e: MessageEvent) => { if (e.data.error) { @@ -73,8 +70,11 @@ export default class IframeDriver extends SqliteLikeBaseDriver { ); }); } +} - close(): void { - // do nothing - } +export default class IframeDriver extends SqliteLikeBaseDriver { + protected conn = new IframeConnection(); + listen = this.conn.listen; + query = this.conn.query; + transaction = this.conn.transaction; } diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts new file mode 100644 index 0000000..7cc4e5c --- /dev/null +++ b/src/drivers/mysql/mysql-driver.ts @@ -0,0 +1,5 @@ +import CommonSQLImplement from "../common-sql-imp"; + +export default abstract class MySQLLikeDriver extends CommonSQLImplement { + +} diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index 92ea63e..a5a2d2d 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -1,38 +1,25 @@ -import { validateOperation } from "@/components/lib/validation"; import type { DatabaseResultSet, DatabaseSchemaItem, DatabaseSchemas, DatabaseTableColumn, - DatabaseTableOperation, - DatabaseTableOperationReslt, DatabaseTableSchema, DatabaseTriggerSchema, DatabaseValue, DriverFlags, SelectFromTableOptions, } from "./base-driver"; -import { BaseDriver } from "./base-driver"; - -import { - escapeSqlValue, - generateInsertStatement, - generateDeleteStatement, - generateUpdateStatement, - generateSelectOneWithConditionStatement, -} from "@/drivers/sqlite/sql-helper"; +import { escapeSqlValue } from "@/drivers/sqlite/sql-helper"; import { parseCreateTableScript } from "@/drivers/sqlite/sql-parse-table"; import { parseCreateTriggerScript } from "@/drivers/sqlite/sql-parse-trigger"; +import CommonSQLImplement from "./common-sql-imp"; -export abstract class SqliteLikeBaseDriver extends BaseDriver { +export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { protected escapeId(id: string) { return `"${id.replace(/"/g, '""')}"`; } - abstract override query(stmt: string): Promise; - abstract override transaction(stmts: string[]): Promise; - getFlags(): DriverFlags { return { supportBigInt: false, @@ -191,102 +178,4 @@ export abstract class SqliteLikeBaseDriver extends BaseDriver { schema: await this.tableSchema(tableName), }; } - - protected validateUpdateOperation( - ops: DatabaseTableOperation[], - validateSchema: DatabaseTableSchema - ) { - for (const op of ops) { - const { valid, reason } = validateOperation(op, validateSchema); - if (!valid) { - throw new Error(reason); - } - } - } - - async updateTableData( - tableName: string, - ops: DatabaseTableOperation[], - validateSchema?: DatabaseTableSchema - ): Promise { - if (validateSchema) { - this.validateUpdateOperation(ops, validateSchema); - } - - const sqls = ops.map((op) => { - if (op.operation === "INSERT") - return generateInsertStatement(tableName, op.values); - if (op.operation === "DELETE") - return generateDeleteStatement(tableName, op.where); - - return generateUpdateStatement(tableName, op.where, op.values); - }); - - const result = await this.transaction(sqls); - console.log("result", result); - - const tmp: DatabaseTableOperationReslt[] = []; - - for (let i = 0; i < result.length; i++) { - const r = result[i]; - const op = ops[i]; - - if (!r || !op) { - tmp.push({}); - continue; - } - - if (op.operation === "UPDATE") { - const selectStatement = generateSelectOneWithConditionStatement( - tableName, - op.where - ); - - // This transform to make it friendly for sending via HTTP - const selectResult = await this.query(selectStatement); - - tmp.push({ - lastId: r.lastInsertRowid, - record: selectResult.rows[0], - }); - } else if (op.operation === "INSERT") { - if (op.autoIncrementPkColumn) { - const selectStatement = generateSelectOneWithConditionStatement( - tableName, - { [op.autoIncrementPkColumn]: r.lastInsertRowid } - ); - - // This transform to make it friendly for sending via HTTP - const selectResult = await this.query(selectStatement); - - tmp.push({ - record: selectResult.rows[0], - lastId: r.lastInsertRowid, - }); - } else if (op.pk && op.pk.length > 0) { - const selectStatement = generateSelectOneWithConditionStatement( - tableName, - op.pk.reduce>((a, b) => { - a[b] = op.values[b]; - return a; - }, {}) - ); - - // This transform to make it friendly for sending via HTTP - const selectResult = await this.query(selectStatement); - - tmp.push({ - record: selectResult.rows[0], - lastId: r.lastInsertRowid, - }); - } else { - tmp.push({}); - } - } else { - tmp.push({}); - } - } - - return tmp; - } } From 41c3de65663f907718e7877d0c9d4bbf1047a678 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 17 Aug 2024 21:26:32 +0700 Subject: [PATCH 02/17] feat: add mysql driver init code --- src/app/(theme)/embed/mysql/page.tsx | 23 +++ src/app/(theme)/embed/sqlite/page.tsx | 4 +- src/components/gui/database-gui.tsx | 2 +- .../gui/schema-editor/column-fk-popup.tsx | 1 + .../gui/schema-editor/schema-save-dialog.tsx | 10 +- src/components/gui/schema-sidebar-list.tsx | 25 +++- src/components/gui/schema-sidebar.tsx | 1 + .../gui/table-cell/generic-cell.tsx | 20 +-- .../table-combobox/TableColumnCombobox.tsx | 7 +- src/components/gui/tabs/schema-editor-tab.tsx | 7 +- src/components/gui/tabs/table-data-tab.tsx | 9 +- src/components/lib/sql-execute-helper.ts | 1 + src/components/lib/validation.test.ts | 36 +++-- src/drivers/base-driver.ts | 15 +- src/drivers/common-sql-imp.ts | 81 +++++++++-- src/drivers/iframe-driver.ts | 44 +++++- src/drivers/mysql/mysql-driver.ts | 133 +++++++++++++++++- src/drivers/query-builder.ts | 111 +++++++++++++++ src/drivers/sqlite-base-driver.ts | 115 ++++----------- src/drivers/sqlite/sql-helper.test.ts | 37 ----- src/drivers/sqlite/sql-helper.ts | 69 --------- src/drivers/sqlite/sql-parse-table.test.ts | 4 + src/drivers/sqlite/sql-parse-table.ts | 1 + src/messages/open-tab.tsx | 10 +- 24 files changed, 510 insertions(+), 256 deletions(-) create mode 100644 src/app/(theme)/embed/mysql/page.tsx create mode 100644 src/drivers/query-builder.ts diff --git a/src/app/(theme)/embed/mysql/page.tsx b/src/app/(theme)/embed/mysql/page.tsx new file mode 100644 index 0000000..6952204 --- /dev/null +++ b/src/app/(theme)/embed/mysql/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import MyStudio from "@/components/my-studio"; +import { IframeMySQLDriver } from "@/drivers/iframe-driver"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useMemo } from "react"; + +export default function EmbedPageClient() { + const searchParams = useSearchParams(); + const driver = useMemo(() => new IframeMySQLDriver(), []); + + useEffect(() => { + return driver.listen(); + }, [driver]); + + return ( + + ); +} diff --git a/src/app/(theme)/embed/sqlite/page.tsx b/src/app/(theme)/embed/sqlite/page.tsx index 225e87a..ed6c6ca 100644 --- a/src/app/(theme)/embed/sqlite/page.tsx +++ b/src/app/(theme)/embed/sqlite/page.tsx @@ -1,13 +1,13 @@ "use client"; import MyStudio from "@/components/my-studio"; -import IframeDriver from "@/drivers/iframe-driver"; +import { IframeSQLiteDriver } from "@/drivers/iframe-driver"; import { useSearchParams } from "next/navigation"; import { useEffect, useMemo } from "react"; export default function EmbedPageClient() { const searchParams = useSearchParams(); - const driver = useMemo(() => new IframeDriver(), []); + const driver = useMemo(() => new IframeSQLiteDriver(), []); useEffect(() => { return driver.listen(); diff --git a/src/components/gui/database-gui.tsx b/src/components/gui/database-gui.tsx index 0300a49..b8678df 100644 --- a/src/components/gui/database-gui.tsx +++ b/src/components/gui/database-gui.tsx @@ -122,7 +122,7 @@ export default function DatabaseGui() { { text: "New Table", onClick: () => { - openTab({ type: "schema" }); + openTab({ type: "schema", schemaName: "main" }); }, }, ]; diff --git a/src/components/gui/schema-editor/column-fk-popup.tsx b/src/components/gui/schema-editor/column-fk-popup.tsx index 459bf1d..8787cd7 100644 --- a/src/components/gui/schema-editor/column-fk-popup.tsx +++ b/src/components/gui/schema-editor/column-fk-popup.tsx @@ -64,6 +64,7 @@ export default function ColumnForeignKeyPopup({ }, }); }} + schemaName={"main"} tableName={constraint.foreignTableName} /> diff --git a/src/components/gui/schema-editor/schema-save-dialog.tsx b/src/components/gui/schema-editor/schema-save-dialog.tsx index 7d31192..e7f6722 100644 --- a/src/components/gui/schema-editor/schema-save-dialog.tsx +++ b/src/components/gui/schema-editor/schema-save-dialog.tsx @@ -20,11 +20,13 @@ import { useCallback, useState } from "react"; import SchemaEditorTab from "../tabs/schema-editor-tab"; export default function SchemaSaveDialog({ + schemaName, schema, previewScript, onClose, fetchTable, }: { + schemaName: string; schema: DatabaseTableSchemaChange; previewScript: string[]; onClose: () => void; @@ -44,7 +46,12 @@ export default function SchemaSaveDialog({ if (schema.name.new !== schema.name.old) { refreshSchema(); replaceCurrentTab({ - component: , + component: ( + + ), key: "_schema_" + schema.name.new, identifier: "_schema_" + schema.name.new, title: "Edit " + schema.name.new, @@ -60,6 +67,7 @@ export default function SchemaSaveDialog({ }); }, [ onClose, + schemaName, databaseDriver, schema, fetchTable, diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index 597f677..de02e85 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -1,4 +1,4 @@ -import { LucideCog, LucideView, Table2 } from "lucide-react"; +import { LucideCog, LucideDatabase, LucideView, Table2 } from "lucide-react"; import { OpenContextMenuList } from "@/messages/open-context-menu"; import { useCallback, useEffect, useMemo, useState } from "react"; import { openTab } from "@/messages/open-tab"; @@ -29,7 +29,7 @@ function prepareListViewItem( data: s, icon: icon, iconColor: iconClassName, - key: s.name, + key: s.schemaName + "." + s.name, name: s.name, }; }); @@ -90,7 +90,7 @@ function groupByFtsTable(items: ListViewItem[]) { export default function SchemaList({ search }: Readonly) { const [selected, setSelected] = useState(""); const [collapsed, setCollapsed] = useState(new Set()); - const { refresh, currentSchema } = useSchema(); + const { refresh, schema } = useSchema(); useEffect(() => { setSelected(""); @@ -115,6 +115,7 @@ export default function SchemaList({ search }: Readonly) { onClick: () => { openTab({ type: "schema", + schemaName: item?.schemaName ?? "", }); }, }, @@ -125,6 +126,7 @@ export default function SchemaList({ search }: Readonly) { openTab({ tableName: item?.name, type: "schema", + schemaName: item?.schemaName ?? "", }); }, } @@ -137,10 +139,18 @@ export default function SchemaList({ search }: Readonly) { ); const filteredSchema = useMemo(() => { - return groupByFtsTable( - groupTriggerByTable(prepareListViewItem(currentSchema)) - ); - }, [currentSchema]); + return Object.entries(schema).map(([s, tables]) => { + return { + data: {}, + icon: LucideDatabase, + name: s, + key: s.toString(), + children: groupByFtsTable( + groupTriggerByTable(prepareListViewItem(tables)) + ), + } as ListViewItem; + }); + }, [schema]); const filterCallback = useCallback( (item: ListViewItem) => { @@ -165,6 +175,7 @@ export default function SchemaList({ search }: Readonly) { 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") { diff --git a/src/components/gui/schema-sidebar.tsx b/src/components/gui/schema-sidebar.tsx index 03bad6f..b8bcadb 100644 --- a/src/components/gui/schema-sidebar.tsx +++ b/src/components/gui/schema-sidebar.tsx @@ -10,6 +10,7 @@ export default function SchemaView() { const onNewTable = useCallback(() => { openTab({ type: "schema", + schemaName: "main", }); }, []); diff --git a/src/components/gui/table-cell/generic-cell.tsx b/src/components/gui/table-cell/generic-cell.tsx index 08e0947..95b53dc 100644 --- a/src/components/gui/table-cell/generic-cell.tsx +++ b/src/components/gui/table-cell/generic-cell.tsx @@ -12,16 +12,10 @@ import { import { DatabaseResultSet, DatabaseValue, - describeTableColumnType, TableColumnDataType, } from "@/drivers/base-driver"; import { useDatabaseDriver } from "@/context/driver-provider"; import { convertDatabaseValueToString } from "@/drivers/sqlite/sql-helper"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; interface TableCellProps { align?: "left" | "right"; @@ -35,21 +29,27 @@ interface TableCellProps { } interface SneakpeakProps { + fkSchemaName: string; fkTableName: string; fkColumnName: string; value: DatabaseValue; } -function SnippetRow({ fkTableName, fkColumnName, value }: SneakpeakProps) { +function SnippetRow({ + fkSchemaName, + fkTableName, + fkColumnName, + value, +}: SneakpeakProps) { const { databaseDriver } = useDatabaseDriver(); const [data, setData] = useState(); useEffect(() => { databaseDriver - .findFirst(fkTableName, { [fkColumnName]: value }) + .findFirst(fkSchemaName, fkTableName, { [fkColumnName]: value }) .then(setData) .catch(console.error); - }, [databaseDriver, fkTableName, fkColumnName, value]); + }, [databaseDriver, fkSchemaName, fkTableName, fkColumnName, value]); if (!data) { return ( @@ -177,7 +177,6 @@ function BlobCellValue({ export default function GenericCell({ value, - valueType, onFocus, isChanged, focus, @@ -206,6 +205,7 @@ export default function GenericCell({ return (
void; @@ -32,11 +34,12 @@ export default function TableColumnCombobox({ useEffect(() => { if (tableName) { databaseDriver - .tableSchema(tableName) + .tableSchema(schemaName, tableName) .then(setSchema) .catch(() => { setSchema({ tableName, + schemaName: "main", columns: [], pk: [], autoIncrement: false, @@ -44,7 +47,7 @@ export default function TableColumnCombobox({ }); }); } - }, [tableName, databaseDriver]); + }, [schemaName, tableName, databaseDriver]); return ( diff --git a/src/components/gui/tabs/schema-editor-tab.tsx b/src/components/gui/tabs/schema-editor-tab.tsx index 1d2506a..017ffea 100644 --- a/src/components/gui/tabs/schema-editor-tab.tsx +++ b/src/components/gui/tabs/schema-editor-tab.tsx @@ -9,6 +9,7 @@ import SchemaSaveDialog from "../schema-editor/schema-save-dialog"; interface SchemaEditorTabProps { tableName?: string; + schemaName: string; } const EMPTY_SCHEMA: DatabaseTableSchemaChange = { @@ -22,6 +23,7 @@ const EMPTY_SCHEMA: DatabaseTableSchemaChange = { }; export default function SchemaEditorTab({ + schemaName, tableName, }: Readonly) { const { databaseDriver } = useDatabaseDriver(); @@ -32,7 +34,7 @@ export default function SchemaEditorTab({ const fetchTable = useCallback( async (name: string) => { databaseDriver - .tableSchema(name) + .tableSchema(schemaName, name) .then((schema) => { setSchema({ name: { @@ -54,7 +56,7 @@ export default function SchemaEditorTab({ .catch(console.error) .finally(() => setLoading(false)); }, - [databaseDriver, setSchema] + [schemaName, databaseDriver, setSchema] ); useEffect(() => { @@ -102,6 +104,7 @@ export default function SchemaEditorTab({ <> {isSaving && ( (); @@ -74,7 +78,7 @@ export default function TableDataWindow({ tableName }: TableDataContentProps) { try { const { data: dataResult, schema: schemaResult } = - await databaseDriver.selectTable(tableName, { + await databaseDriver.selectTable(schemaName, tableName, { whereRaw: where, limit: finalLimit, offset: finalOffset, @@ -99,6 +103,7 @@ export default function TableDataWindow({ tableName }: TableDataContentProps) { }, [ databaseDriver, tableName, + schemaName, sortColumns, updateTableSchema, setStat, diff --git a/src/components/lib/sql-execute-helper.ts b/src/components/lib/sql-execute-helper.ts index 2d0c238..5ed1f3b 100644 --- a/src/components/lib/sql-execute-helper.ts +++ b/src/components/lib/sql-execute-helper.ts @@ -86,6 +86,7 @@ export async function commitChange({ try { const result = await driver.updateTableData( + tableSchema.schemaName ?? "main", tableName, plans.map((p) => p.plan), tableSchema diff --git a/src/components/lib/validation.test.ts b/src/components/lib/validation.test.ts index cbc7211..78b04c5 100644 --- a/src/components/lib/validation.test.ts +++ b/src/components/lib/validation.test.ts @@ -12,7 +12,7 @@ describe("Operation Validation", () => { id: 5, }, }, - { autoIncrement: false, columns: [], pk: ["id"] } + { autoIncrement: false, schemaName: "main", columns: [], pk: ["id"] } ); expect(result.valid).toBe(true); @@ -29,7 +29,7 @@ describe("Operation Validation", () => { id: 5, }, }, - { autoIncrement: false, columns: [], pk: [""] } + { autoIncrement: false, schemaName: "main", columns: [], pk: [""] } ); expect(result.valid).toBe(false); @@ -46,7 +46,7 @@ describe("Operation Validation", () => { id: null, }, }, - { autoIncrement: false, columns: [], pk: ["id"] } + { autoIncrement: false, schemaName: "main", columns: [], pk: ["id"] } ); expect(result.valid).toBe(false); @@ -64,7 +64,12 @@ describe("Operation Validation", () => { name: null, }, }, - { autoIncrement: false, columns: [], pk: ["id", "name"] } + { + autoIncrement: false, + schemaName: "main", + columns: [], + pk: ["id", "name"], + } ); expect(result.valid).toBe(false); @@ -81,7 +86,7 @@ describe("Operation Validation", () => { id: 5, }, }, - { autoIncrement: false, columns: [], pk: ["id"] } + { autoIncrement: false, schemaName: "main", columns: [], pk: ["id"] } ); expect(result.valid).toBe(false); @@ -95,7 +100,7 @@ describe("Operation Validation", () => { id: 5, }, }, - { autoIncrement: false, columns: [], pk: ["id"] } + { autoIncrement: false, schemaName: "main", columns: [], pk: ["id"] } ); expect(result.valid).toBe(true); @@ -109,7 +114,7 @@ describe("Operation Validation", () => { id: 5, }, }, - { autoIncrement: false, columns: [], pk: [] } + { autoIncrement: false, schemaName: "main", columns: [], pk: [] } ); expect(result.valid).toBe(false); @@ -123,7 +128,7 @@ describe("Operation Validation", () => { id: null, }, }, - { autoIncrement: false, columns: [], pk: [] } + { autoIncrement: false, schemaName: "main", columns: [], pk: [] } ); expect(result.valid).toBe(false); @@ -138,7 +143,7 @@ describe("Operation Validation", () => { name: "Visal", }, }, - { autoIncrement: false, columns: [], pk: ["id"] } + { autoIncrement: false, schemaName: "main", columns: [], pk: ["id"] } ); expect(result.valid).toBe(true); @@ -153,7 +158,7 @@ describe("Operation Validation", () => { name: "Visal", }, }, - { autoIncrement: false, columns: [], pk: ["id"] } + { autoIncrement: false, schemaName: "main", columns: [], pk: ["id"] } ); expect(result.valid).toBe(false); @@ -168,7 +173,7 @@ describe("Operation Validation", () => { name: "Visal", }, }, - { autoIncrement: true, columns: [], pk: ["id"] } + { autoIncrement: true, schemaName: "main", columns: [], pk: ["id"] } ); expect(result.valid).toBe(false); @@ -183,7 +188,12 @@ describe("Operation Validation", () => { name: null, }, }, - { autoIncrement: false, columns: [], pk: ["id", "name"] } + { + autoIncrement: false, + schemaName: "main", + columns: [], + pk: ["id", "name"], + } ); expect(result.valid).toBe(false); @@ -197,7 +207,7 @@ describe("Operation Validation", () => { name: "Visal", }, }, - { autoIncrement: true, columns: [], pk: ["id"] } + { autoIncrement: true, schemaName: "main", columns: [], pk: ["id"] } ); expect(result.valid).toBe(true); diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index 665049e..bef0c3d 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -74,6 +74,7 @@ export type DatabaseSchemas = Record; export interface DatabaseSchemaItem { type: "table" | "trigger" | "view"; name: string; + schemaName: string; tableName?: string; tableSchema?: DatabaseTableSchema; } @@ -145,6 +146,7 @@ export interface DatabaseTableSchema { columns: DatabaseTableColumn[]; pk: string[]; autoIncrement: boolean; + schemaName: string; tableName?: string; constraints?: DatabaseTableColumnConstraint[]; createScript?: string; @@ -203,6 +205,10 @@ export abstract class BaseDriver { // Flags abstract getFlags(): DriverFlags; + // Helper class + abstract escapeId(id: string): string; + abstract escapeValue(value: unknown): string; + // Methods abstract close(): void; @@ -210,20 +216,27 @@ export abstract class BaseDriver { abstract transaction(stmts: string[]): Promise; abstract schemas(): Promise; - abstract tableSchema(tableName: string): Promise; + abstract tableSchema( + schemaName: string, + tableName: string + ): Promise; + abstract trigger(name: string): Promise; abstract findFirst( + schemaName: string, tableName: string, key: Record ): Promise; abstract selectTable( + schemaName: string, tableName: string, options: SelectFromTableOptions ): Promise<{ data: DatabaseResultSet; schema: DatabaseTableSchema }>; abstract updateTableData( + schemaName: string, tableName: string, ops: DatabaseTableOperation[], diff --git a/src/drivers/common-sql-imp.ts b/src/drivers/common-sql-imp.ts index a3a7675..5a48e58 100644 --- a/src/drivers/common-sql-imp.ts +++ b/src/drivers/common-sql-imp.ts @@ -1,16 +1,20 @@ import { validateOperation } from "@/components/lib/validation"; import { BaseDriver, + DatabaseResultSet, DatabaseTableOperation, DatabaseTableOperationReslt, DatabaseTableSchema, + DatabaseValue, + SelectFromTableOptions, } from "./base-driver"; +import { escapeSqlValue } from "./sqlite/sql-helper"; import { - generateDeleteStatement, - generateInsertStatement, - generateSelectOneWithConditionStatement, - generateUpdateStatement, -} from "./sqlite/sql-helper"; + deleteFrom, + insertInto, + selectFrom, + updateTable, +} from "./query-builder"; export default abstract class CommonSQLImplement extends BaseDriver { protected validateUpdateOperation( @@ -26,6 +30,7 @@ export default abstract class CommonSQLImplement extends BaseDriver { } async updateTableData( + schemaName: string, tableName: string, ops: DatabaseTableOperation[], validateSchema?: DatabaseTableSchema @@ -36,11 +41,11 @@ export default abstract class CommonSQLImplement extends BaseDriver { const sqls = ops.map((op) => { if (op.operation === "INSERT") - return generateInsertStatement(tableName, op.values); + return insertInto(this, schemaName, tableName, op.values); if (op.operation === "DELETE") - return generateDeleteStatement(tableName, op.where); + return deleteFrom(this, schemaName, tableName, op.where, 1); - return generateUpdateStatement(tableName, op.where, op.values); + return updateTable(this, schemaName, tableName, op.where, op.values, 1); }); const result = await this.transaction(sqls); @@ -57,9 +62,12 @@ export default abstract class CommonSQLImplement extends BaseDriver { } if (op.operation === "UPDATE") { - const selectStatement = generateSelectOneWithConditionStatement( + const selectStatement = selectFrom( + this, + schemaName, tableName, - op.where + op.where, + { limit: 1, offset: 0 } ); // This transform to make it friendly for sending via HTTP @@ -71,9 +79,12 @@ export default abstract class CommonSQLImplement extends BaseDriver { }); } else if (op.operation === "INSERT") { if (op.autoIncrementPkColumn) { - const selectStatement = generateSelectOneWithConditionStatement( + const selectStatement = selectFrom( + this, + schemaName, tableName, - { [op.autoIncrementPkColumn]: r.lastInsertRowid } + { [op.autoIncrementPkColumn]: r.lastInsertRowid }, + { limit: 1, offset: 0 } ); // This transform to make it friendly for sending via HTTP @@ -84,12 +95,15 @@ export default abstract class CommonSQLImplement extends BaseDriver { lastId: r.lastInsertRowid, }); } else if (op.pk && op.pk.length > 0) { - const selectStatement = generateSelectOneWithConditionStatement( + const selectStatement = selectFrom( + this, + schemaName, tableName, op.pk.reduce>((a, b) => { a[b] = op.values[b]; return a; - }, {}) + }, {}), + { limit: 1, offset: 0 } ); // This transform to make it friendly for sending via HTTP @@ -109,4 +123,43 @@ export default abstract class CommonSQLImplement extends BaseDriver { return tmp; } + + async findFirst( + schemaName: string, + tableName: string, + key: Record + ): Promise { + const wherePart = Object.entries(key) + .map(([colName, colValue]) => { + return `${this.escapeId(colName)} = ${escapeSqlValue(colValue)}`; + }) + .join(", "); + + const sql = `SELECT * FROM ${this.escapeId(schemaName)}.${this.escapeId(tableName)} ${wherePart ? "WHERE " + wherePart : ""} LIMIT 1 OFFSET 0`; + return this.query(sql); + } + + async selectTable( + schemaName: string, + tableName: string, + options: SelectFromTableOptions + ): Promise<{ data: DatabaseResultSet; schema: DatabaseTableSchema }> { + const whereRaw = options.whereRaw?.trim(); + + const orderPart = + options.orderBy && options.orderBy.length > 0 + ? options.orderBy + .map((r) => `${this.escapeId(r.columnName)} ${r.by}`) + .join(", ") + : ""; + + const sql = `SELECT * FROM ${this.escapeId(schemaName)}.${this.escapeId(tableName)}${ + whereRaw ? ` WHERE ${whereRaw} ` : "" + } ${orderPart ? ` ORDER BY ${orderPart}` : ""} LIMIT ${escapeSqlValue(options.limit)} OFFSET ${escapeSqlValue(options.offset)};`; + + return { + data: await this.query(sql), + schema: await this.tableSchema(schemaName, tableName), + }; + } } diff --git a/src/drivers/iframe-driver.ts b/src/drivers/iframe-driver.ts index 6906268..04fb66a 100644 --- a/src/drivers/iframe-driver.ts +++ b/src/drivers/iframe-driver.ts @@ -1,4 +1,5 @@ import { DatabaseResultSet } from "./base-driver"; +import MySQLLikeDriver from "./mysql/mysql-driver"; import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; type ParentResponseData = @@ -44,6 +45,15 @@ class IframeConnection { const id = ++this.counter; this.queryPromise[id] = { resolve, reject }; + console.log( + "POST " + + { + type: "query", + id, + statement: stmt, + } + ); + window.parent.postMessage( { type: "query", @@ -60,6 +70,15 @@ class IframeConnection { const id = ++this.counter; this.queryPromise[id] = { resolve, reject }; + console.log( + "POST " + + { + type: "transaction", + id, + statement: stmts, + } + ); + window.parent.postMessage( { type: "transaction", @@ -72,9 +91,32 @@ class IframeConnection { } } -export default class IframeDriver extends SqliteLikeBaseDriver { +export class IframeSQLiteDriver extends SqliteLikeBaseDriver { protected conn = new IframeConnection(); listen = this.conn.listen; query = this.conn.query; transaction = this.conn.transaction; + close() {} +} + +export class IframeMySQLDriver extends MySQLLikeDriver { + protected conn = new IframeConnection(); + + listen() { + this.conn.listen(); + } + + close(): void {} + + async query(stmt: string): Promise { + const r = await this.conn.query(stmt); + console.log(r); + return r; + } + + transaction(stmts: string[]): Promise { + const r = this.conn.transaction(stmts); + console.log(r); + return r; + } } diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index 7cc4e5c..f5a1d96 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -1,5 +1,136 @@ +import { + DatabaseSchemas, + DatabaseTableSchema, + DatabaseTriggerSchema, + DriverFlags, + DatabaseSchemaItem, + DatabaseTableColumn, +} from "../base-driver"; import CommonSQLImplement from "../common-sql-imp"; +import { escapeSqlValue } from "../sqlite/sql-helper"; + +interface MySqlDatabase { + SCHEMA_NAME: string; +} + +interface MySqlColumn { + TABLE_SCHEMA: string; + TABLE_NAME: string; + COLUMN_NAME: string; + DATA_TYPE: string; + IS_NULLABLE: "YES" | "NO"; + COLUMN_COMMENT: string; + CHARACTER_MAXIMUM_LENGTH: number; + NUMERIC_PRECISION: number; + NUMERIC_SCALE: number; + COLUMN_DEFAULT: string; + COLUMN_TYPE: string; +} + +interface MySqlTable { + TABLE_SCHEMA: string; + TABLE_NAME: string; + TABLE_TYPE: string; +} export default abstract class MySQLLikeDriver extends CommonSQLImplement { - + escapeId(id: string) { + return `\`${id.replace(/`/g, "``")}\``; + } + + escapeValue(value: unknown): string { + return escapeSqlValue(value); + } + + getFlags(): DriverFlags { + return { + defaultSchema: "", + optionalSchema: false, + supportBigInt: false, + }; + } + + async schemas(): Promise { + const schemaSql = "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA"; + const schemaResult = (await this.query(schemaSql)) + .rows as unknown as MySqlDatabase[]; + + const tableSql = + "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables"; + const tableResult = (await this.query(tableSql)) + .rows as unknown as MySqlTable[]; + + const columnSql = + "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, EXTRA FROM information_schema.columns"; + const columnResult = (await this.query(columnSql)) + .rows as unknown as MySqlColumn[]; + + // Hash table of schema + const schemaRecord: Record = {}; + for (const s of schemaResult) { + schemaRecord[s.SCHEMA_NAME] = []; + } + + // Hash table of table + const tableRecord: Record = {}; + for (const t of tableResult) { + const key = t.TABLE_SCHEMA + "." + t.TABLE_NAME; + const table: DatabaseSchemaItem = { + name: t.TABLE_NAME, + type: t.TABLE_TYPE === "VIEW" ? "view" : "table", + tableName: t.TABLE_NAME, + schemaName: t.TABLE_SCHEMA, + tableSchema: { + autoIncrement: false, + pk: [], + columns: [], + tableName: t.TABLE_NAME, + schemaName: t.TABLE_SCHEMA, + }, + }; + + tableRecord[key] = table; + if (schemaRecord[t.TABLE_SCHEMA]) { + schemaRecord[t.TABLE_SCHEMA].push(table); + } + } + + for (const c of columnResult) { + const column: DatabaseTableColumn = { + name: c.COLUMN_NAME, + type: c.COLUMN_TYPE, + }; + + const tableKey = c.TABLE_SCHEMA + "." + c.TABLE_NAME; + if (tableRecord[tableKey].tableSchema) { + tableRecord[tableKey].tableSchema.columns.push(column); + } + } + + return schemaRecord; + } + + async tableSchema( + schemaName: string, + tableName: string + ): Promise { + const columnSql = `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, EXTRA FROM information_schema.columns WHERE TABLE_NAME=${escapeSqlValue(tableName)} AND TABLE_SCHEMA=${escapeSqlValue(schemaName)}`; + const columnResult = (await this.query(columnSql)) + .rows as unknown as MySqlColumn[]; + + return { + autoIncrement: false, + pk: [], + tableName, + schemaName, + columns: columnResult.map((c) => ({ + name: c.COLUMN_NAME, + type: c.COLUMN_TYPE, + })), + }; + } + + trigger(): Promise { + throw new Error("Not implemented"); + } } diff --git a/src/drivers/query-builder.ts b/src/drivers/query-builder.ts new file mode 100644 index 0000000..12c140c --- /dev/null +++ b/src/drivers/query-builder.ts @@ -0,0 +1,111 @@ +import { BaseDriver } from "./base-driver"; + +function generateWhere(dialect: BaseDriver, where: Record) { + const conditions = Object.entries(where) + .map(([columnName, value]) => { + return `${dialect.escapeId(columnName)} = ${dialect.escapeValue(value)}`; + }) + .join(" AND "); + + if (conditions.length > 0) return "WHERE " + conditions; + return null; +} + +function generateSet(dialect: BaseDriver, where: Record) { + return Object.entries(where) + .map(([columnName, value]) => { + return `${dialect.escapeId(columnName)} = ${dialect.escapeValue(value)}`; + }) + .join(", "); +} + +function generateInsertValue( + dialect: BaseDriver, + values: Record +) { + const columnNameList: string[] = []; + const valueList: string[] = []; + + for (const [columnName, value] of Object.entries(values)) { + columnNameList.push(dialect.escapeId(columnName)); + valueList.push(dialect.escapeValue(value)); + } + + return `(${columnNameList.join(", ")}) VALUES(${valueList.join(", ")})`; +} + +function generateLimit(limit?: number, offset?: number) { + if (!Number.isInteger(limit)) return null; + if (!Number.isInteger(offset)) return null; + + return `LIMIT ${limit} OFFSET ${offset}`; +} + +export function selectFrom( + dialect: BaseDriver, + schema: string, + table: string, + where: Record, + options?: { limit?: number; offset?: number } +): string { + return [ + "SELECT", + "*", + "FROM", + `${dialect.escapeId(schema)}.${dialect.escapeId(table)}`, + generateWhere(dialect, where), + generateLimit(options?.limit, options?.offset), + ] + .filter(Boolean) + .join(" "); +} + +export function insertInto( + dialect: BaseDriver, + schema: string, + table: string, + value: Record +) { + return [ + "INSERT INTO", + `${dialect.escapeId(schema)}.${dialect.escapeId(table)}`, + generateInsertValue(dialect, value), + ].join(); +} + +export function updateTable( + dialect: BaseDriver, + schema: string, + table: string, + value: Record, + where: Record, + limit?: number +): string { + return [ + "UPDATE", + `${dialect.escapeId(schema)}.${dialect.escapeId(table)}`, + "SET", + generateSet(dialect, value), + generateWhere(dialect, where), + limit ? `LIMIT ${limit}` : null, + ] + .filter(Boolean) + .join(); +} + +export function deleteFrom( + dialect: BaseDriver, + schema: string, + table: string, + where: Record, + limit?: number +): string { + return [ + "DELETE FROM", + `${dialect.escapeId(schema)}.${dialect.escapeId(table)}`, + generateWhere(dialect, where), + limit ? `LIMIT ${limit}` : null, + ] + .filter(Boolean) + .join(); +} diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index a5a2d2d..c51a583 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -1,13 +1,9 @@ import type { - DatabaseResultSet, DatabaseSchemaItem, DatabaseSchemas, - DatabaseTableColumn, DatabaseTableSchema, DatabaseTriggerSchema, - DatabaseValue, DriverFlags, - SelectFromTableOptions, } from "./base-driver"; import { escapeSqlValue } from "@/drivers/sqlite/sql-helper"; @@ -16,10 +12,14 @@ import { parseCreateTriggerScript } from "@/drivers/sqlite/sql-parse-trigger"; import CommonSQLImplement from "./common-sql-imp"; export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { - protected escapeId(id: string) { + escapeId(id: string) { return `"${id.replace(/"/g, '""')}"`; } + escapeValue(value: unknown): string { + return escapeSqlValue(value); + } + getFlags(): DriverFlags { return { supportBigInt: false, @@ -44,16 +44,22 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { try { tmp.push({ type: "table", + schemaName: "main", name: row.name, tableSchema: parseCreateTableScript(row.sql), }); } catch { - tmp.push({ type: "table", name: row.name }); + tmp.push({ type: "table", name: row.name, schemaName: "main" }); } } else if (row.type === "trigger") { - tmp.push({ type: "trigger", name: row.name, tableName: row.tbl_name }); + tmp.push({ + type: "trigger", + name: row.name, + tableName: row.tbl_name, + schemaName: "main", + }); } else if (row.type === "view") { - tmp.push({ type: "view", name: row.name }); + tmp.push({ type: "view", name: row.name, schemaName: "main" }); } } @@ -79,50 +85,10 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { // do nothing } - protected async legacyTableSchema( + async tableSchema( + schemaName: string, tableName: string ): Promise { - const sql = `SELECT * FROM pragma_table_info(${this.escapeId(tableName)});`; - const result = await this.query(sql); - - const rows = result.rows as Array<{ - name: string; - type: string; - pk: number; - }>; - - const columns: DatabaseTableColumn[] = rows.map((row) => ({ - name: row.name, - type: row.type, - pk: !!row.pk, - })); - - // Check auto increment - let hasAutoIncrement = false; - - try { - const seqCount = await this.query( - `SELECT COUNT(*) AS total FROM sqlite_sequence WHERE name=${escapeSqlValue( - tableName - )};` - ); - - const seqRow = seqCount.rows[0]; - if (seqRow && Number(seqRow[0] ?? 0) > 0) { - hasAutoIncrement = true; - } - } catch (e) { - console.error(e); - } - - return { - columns, - pk: columns.filter((col) => col.pk).map((col) => col.name), - autoIncrement: hasAutoIncrement, - }; - } - - async tableSchema(tableName: string): Promise { const sql = `SELECT * FROM sqlite_schema WHERE tbl_name = ${escapeSqlValue( tableName )};`; @@ -133,49 +99,16 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { const def = rows.find((row) => row.type === "table"); if (def) { const createScript = def.sql; - return { ...parseCreateTableScript(createScript), createScript }; + return { + ...parseCreateTableScript(createScript), + createScript, + schemaName, + }; } + + throw new Error("Unexpected error finding table " + tableName); } catch (e) { - console.error(e); + throw new Error("Unexpected error while parsing"); } - - return await this.legacyTableSchema(tableName); - } - - async findFirst( - tableName: string, - key: Record - ): Promise { - const wherePart = Object.entries(key) - .map(([colName, colValue]) => { - return `${this.escapeId(colName)} = ${escapeSqlValue(colValue)}`; - }) - .join(", "); - - const sql = `SELECT * FROM ${this.escapeId(tableName)} ${wherePart ? "WHERE " + wherePart : ""} LIMIT 1 OFFSET 0`; - return this.query(sql); - } - - async selectTable( - tableName: string, - options: SelectFromTableOptions - ): Promise<{ data: DatabaseResultSet; schema: DatabaseTableSchema }> { - const whereRaw = options.whereRaw?.trim(); - - const orderPart = - options.orderBy && options.orderBy.length > 0 - ? options.orderBy - .map((r) => `${this.escapeId(r.columnName)} ${r.by}`) - .join(", ") - : ""; - - const sql = `SELECT * FROM ${this.escapeId(tableName)}${ - whereRaw ? ` WHERE ${whereRaw} ` : "" - } ${orderPart ? ` ORDER BY ${orderPart}` : ""} LIMIT ${escapeSqlValue(options.limit)} OFFSET ${escapeSqlValue(options.offset)};`; - - return { - data: await this.query(sql), - schema: await this.tableSchema(tableName), - }; } } diff --git a/src/drivers/sqlite/sql-helper.test.ts b/src/drivers/sqlite/sql-helper.test.ts index c955a60..1e82104 100644 --- a/src/drivers/sqlite/sql-helper.test.ts +++ b/src/drivers/sqlite/sql-helper.test.ts @@ -5,9 +5,6 @@ import { escapeSqlBinary, escapeSqlString, escapeSqlValue, - generateDeleteStatement, - generateInsertStatement, - generateUpdateStatement, selectStatementFromPosition, unescapeIdentity, } from "./sql-helper"; @@ -41,40 +38,6 @@ describe("Escape SQL", () => { }); }); -describe("Generate SQL Statement", () => { - it("Generate insert statement from object", () => { - expect( - generateInsertStatement("users", { - name: "Visal", - age: 50, - title: "O'reilly", - }) - ).toBe( - `INSERT INTO "users"("name", "age", "title") VALUES('Visal', 50, 'O''reilly');` - ); - }); - - it("Generate update statement", () => { - expect( - generateUpdateStatement( - "users", - { - id: 5, - }, - { age: 50, title: "O'reilly" } - ) - ).toBe( - `UPDATE "users" SET "age" = 50, "title" = 'O''reilly' WHERE "id" = 5;` - ); - }); - - it("Generate delete statement", () => { - expect(generateDeleteStatement("users", { id: 5 })).toBe( - `DELETE FROM "users" WHERE "id" = 5;` - ); - }); -}); - describe("Mapping sqlite column type to our table type", () => { const integerType = [ "INT", diff --git a/src/drivers/sqlite/sql-helper.ts b/src/drivers/sqlite/sql-helper.ts index 8f13958..77aacd1 100644 --- a/src/drivers/sqlite/sql-helper.ts +++ b/src/drivers/sqlite/sql-helper.ts @@ -62,75 +62,6 @@ export function convertSqliteType( return TableColumnDataType.TEXT; } -export function generateSelectOneWithConditionStatement( - tableName: string, - condition: Record -) { - const wherePart = Object.entries(condition) - .map( - ([columnName, value]) => - `${escapeIdentity(columnName)} = ${escapeSqlValue(value)}` - ) - .join(" AND "); - - return `SELECT * FROM ${escapeIdentity( - tableName - )} WHERE ${wherePart} LIMIT 1 OFFSET 0;`; -} - -export function generateInsertStatement( - tableName: string, - value: Record -): string { - const fieldPart: string[] = []; - const valuePart: unknown[] = []; - - for (const entry of Object.entries(value)) { - fieldPart.push(entry[0]); - valuePart.push(entry[1]); - } - return `INSERT INTO ${escapeIdentity(tableName)}(${fieldPart - .map(escapeIdentity) - .join(", ")}) VALUES(${valuePart.map(escapeSqlValue).join(", ")});`; -} - -export function generateDeleteStatement( - tableName: string, - where: Record -) { - const wherePart: string = Object.entries(where) - .map( - ([columnName, value]) => - `${escapeIdentity(columnName)} = ${escapeSqlValue(value)}` - ) - .join(" AND "); - - return `DELETE FROM ${escapeIdentity(tableName)} WHERE ${wherePart};`; -} - -export function generateUpdateStatement( - tableName: string, - where: Record, - changeValue: Record -): string { - const setPart = Object.entries(changeValue) - .map(([colName, value]) => { - return `${escapeIdentity(colName)} = ${escapeSqlValue(value)}`; - }) - .join(", "); - - const wherePart: string = Object.entries(where) - .map( - ([columnName, value]) => - `${escapeIdentity(columnName)} = ${escapeSqlValue(value)}` - ) - .join(" AND "); - - return `UPDATE ${escapeIdentity( - tableName - )} SET ${setPart} WHERE ${wherePart};`; -} - export function selectStatementFromPosition( statements: IdentifyResult[], pos: number diff --git a/src/drivers/sqlite/sql-parse-table.test.ts b/src/drivers/sqlite/sql-parse-table.test.ts index a7200da..e7e954c 100644 --- a/src/drivers/sqlite/sql-parse-table.test.ts +++ b/src/drivers/sqlite/sql-parse-table.test.ts @@ -113,6 +113,7 @@ it("parse create table", () => { pk: ["id"], autoIncrement: true, constraints: [], + schemaName: "main", columns: [ { name: "id", @@ -171,6 +172,7 @@ it("parse create table with table constraints", () => { expect(p(sql)).toEqual({ tableName: "users", + schemaName: "main", pk: ["first_name", "last_name"], autoIncrement: false, constraints: [ @@ -210,6 +212,7 @@ it("parse fts5 virtual table", () => { const sql = `create virtual table name_fts using fts5(name, tokenize='trigram');`; expect(p(sql)).toEqual({ tableName: "name_fts", + schemaName: "main", autoIncrement: false, pk: [], columns: [], @@ -222,6 +225,7 @@ it("parse fts5 virtual table with external content", () => { const sql = `create virtual table name_fts using fts5(name, tokenize='trigram', content='student', content_rowid='id');`; expect(p(sql)).toEqual({ tableName: "name_fts", + schemaName: "main", autoIncrement: false, pk: [], columns: [], diff --git a/src/drivers/sqlite/sql-parse-table.ts b/src/drivers/sqlite/sql-parse-table.ts index 0ded51c..f9584a7 100644 --- a/src/drivers/sqlite/sql-parse-table.ts +++ b/src/drivers/sqlite/sql-parse-table.ts @@ -537,6 +537,7 @@ export function parseCreateTableScript(sql: string): DatabaseTableSchema { return { tableName, + schemaName: "main", ...defs, pk, autoIncrement, diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index 824f761..15ef210 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -16,6 +16,7 @@ import TriggerTab from "@/components/gui/tabs/trigger-tab"; interface OpenTableTab { type: "table"; + schemaName: string; tableName: string; } @@ -31,6 +32,7 @@ interface OpenQueryTab { interface OpenTableSchemaTab { type: "schema"; + schemaName: string; tableName?: string; } @@ -111,9 +113,13 @@ function generateComponent(tab: OpenTabsProps, title: string) { return ; } if (tab.type === "table") - return ; + return ( + + ); if (tab.type === "schema") - return ; + return ( + + ); if (tab.type === "user") return ; return ; } From 365c1ce28d1d840f8bee298aca88f654e958cc18 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 20 Aug 2024 20:01:33 +0700 Subject: [PATCH 03/17] mapping pk and autoincrement --- package.json | 2 +- src/drivers/common-sql-imp.ts | 2 +- src/drivers/iframe-driver.ts | 26 ++++++++++---------------- src/drivers/mysql/mysql-driver.ts | 16 +++++++++++++--- src/drivers/query-builder.ts | 6 +++--- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index da5a57e..fa5b8e6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.6.4", "private": false, "scripts": { - "dev": "next dev", + "dev": "next dev -p 3008", "dev-https": "next dev --experimental-https", "build": "next build", "build-with-migrate": "npm run db:migrate && next build", diff --git a/src/drivers/common-sql-imp.ts b/src/drivers/common-sql-imp.ts index 5a48e58..daa2f9d 100644 --- a/src/drivers/common-sql-imp.ts +++ b/src/drivers/common-sql-imp.ts @@ -45,7 +45,7 @@ export default abstract class CommonSQLImplement extends BaseDriver { if (op.operation === "DELETE") return deleteFrom(this, schemaName, tableName, op.where, 1); - return updateTable(this, schemaName, tableName, op.where, op.values, 1); + return updateTable(this, schemaName, tableName, op.values, op.where, 1); }); const result = await this.transaction(sqls); diff --git a/src/drivers/iframe-driver.ts b/src/drivers/iframe-driver.ts index 04fb66a..779f1c0 100644 --- a/src/drivers/iframe-driver.ts +++ b/src/drivers/iframe-driver.ts @@ -45,14 +45,11 @@ class IframeConnection { const id = ++this.counter; this.queryPromise[id] = { resolve, reject }; - console.log( - "POST " + - { - type: "query", - id, - statement: stmt, - } - ); + console.log("POST ", { + type: "query", + id, + statement: stmt, + }); window.parent.postMessage( { @@ -70,14 +67,11 @@ class IframeConnection { const id = ++this.counter; this.queryPromise[id] = { resolve, reject }; - console.log( - "POST " + - { - type: "transaction", - id, - statement: stmts, - } - ); + console.log("POST ", { + type: "transaction", + id, + statement: stmts, + }); window.parent.postMessage( { diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index f5a1d96..626ee7f 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -17,6 +17,8 @@ interface MySqlColumn { TABLE_SCHEMA: string; TABLE_NAME: string; COLUMN_NAME: string; + EXTRA: string; + COLUMN_KEY: string; DATA_TYPE: string; IS_NULLABLE: "YES" | "NO"; COLUMN_COMMENT: string; @@ -114,13 +116,21 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { schemaName: string, tableName: string ): Promise { - const columnSql = `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, EXTRA FROM information_schema.columns WHERE TABLE_NAME=${escapeSqlValue(tableName)} AND TABLE_SCHEMA=${escapeSqlValue(schemaName)}`; + const columnSql = `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, EXTRA, COLUMN_KEY FROM information_schema.columns WHERE TABLE_NAME=${escapeSqlValue(tableName)} AND TABLE_SCHEMA=${escapeSqlValue(schemaName)} ORDER BY ORDINAL_POSITION`; const columnResult = (await this.query(columnSql)) .rows as unknown as MySqlColumn[]; + const pk = columnResult + .filter((c) => c.COLUMN_KEY === "PRI") + .map((c) => c.COLUMN_NAME); + + const autoIncrement = columnResult.some( + (c) => c.EXTRA === "auto_increment" + ); + return { - autoIncrement: false, - pk: [], + autoIncrement, + pk, tableName, schemaName, columns: columnResult.map((c) => ({ diff --git a/src/drivers/query-builder.ts b/src/drivers/query-builder.ts index 12c140c..4fb0e4f 100644 --- a/src/drivers/query-builder.ts +++ b/src/drivers/query-builder.ts @@ -70,7 +70,7 @@ export function insertInto( "INSERT INTO", `${dialect.escapeId(schema)}.${dialect.escapeId(table)}`, generateInsertValue(dialect, value), - ].join(); + ].join(" "); } export function updateTable( @@ -90,7 +90,7 @@ export function updateTable( limit ? `LIMIT ${limit}` : null, ] .filter(Boolean) - .join(); + .join(" "); } export function deleteFrom( @@ -107,5 +107,5 @@ export function deleteFrom( limit ? `LIMIT ${limit}` : null, ] .filter(Boolean) - .join(); + .join(" "); } From cf201719ac301aefc1f883cfc8fbd211bfa3591d Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Thu, 22 Aug 2024 22:29:34 +0700 Subject: [PATCH 04/17] add attach support --- src/drivers/common-sql-imp.ts | 4 ++-- src/drivers/iframe-driver.ts | 23 ++++++++++++++---- src/drivers/query-builder.ts | 8 ++----- src/drivers/sqlite-base-driver.ts | 40 ++++++++++++++++++++++--------- src/messages/open-tab.tsx | 7 ++++-- 5 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/drivers/common-sql-imp.ts b/src/drivers/common-sql-imp.ts index daa2f9d..957e0d3 100644 --- a/src/drivers/common-sql-imp.ts +++ b/src/drivers/common-sql-imp.ts @@ -43,9 +43,9 @@ export default abstract class CommonSQLImplement extends BaseDriver { if (op.operation === "INSERT") return insertInto(this, schemaName, tableName, op.values); if (op.operation === "DELETE") - return deleteFrom(this, schemaName, tableName, op.where, 1); + return deleteFrom(this, schemaName, tableName, op.where); - return updateTable(this, schemaName, tableName, op.values, op.where, 1); + return updateTable(this, schemaName, tableName, op.values, op.where); }); const result = await this.transaction(sqls); diff --git a/src/drivers/iframe-driver.ts b/src/drivers/iframe-driver.ts index 779f1c0..ea8ee98 100644 --- a/src/drivers/iframe-driver.ts +++ b/src/drivers/iframe-driver.ts @@ -36,6 +36,7 @@ class IframeConnection { } }; + console.log("register listener"); window.addEventListener("message", handler); return () => window.removeEventListener("message", handler); } @@ -87,10 +88,24 @@ class IframeConnection { export class IframeSQLiteDriver extends SqliteLikeBaseDriver { protected conn = new IframeConnection(); - listen = this.conn.listen; - query = this.conn.query; - transaction = this.conn.transaction; - close() {} + + listen() { + this.conn.listen(); + } + + close(): void {} + + async query(stmt: string): Promise { + const r = await this.conn.query(stmt); + console.log(r); + return r; + } + + transaction(stmts: string[]): Promise { + const r = this.conn.transaction(stmts); + console.log(r); + return r; + } } export class IframeMySQLDriver extends MySQLLikeDriver { diff --git a/src/drivers/query-builder.ts b/src/drivers/query-builder.ts index 4fb0e4f..34db110 100644 --- a/src/drivers/query-builder.ts +++ b/src/drivers/query-builder.ts @@ -78,8 +78,7 @@ export function updateTable( schema: string, table: string, value: Record, - where: Record, - limit?: number + where: Record ): string { return [ "UPDATE", @@ -87,7 +86,6 @@ export function updateTable( "SET", generateSet(dialect, value), generateWhere(dialect, where), - limit ? `LIMIT ${limit}` : null, ] .filter(Boolean) .join(" "); @@ -97,14 +95,12 @@ export function deleteFrom( dialect: BaseDriver, schema: string, table: string, - where: Record, - limit?: number + where: Record ): string { return [ "DELETE FROM", `${dialect.escapeId(schema)}.${dialect.escapeId(table)}`, generateWhere(dialect, where), - limit ? `LIMIT ${limit}` : null, ] .filter(Boolean) .join(" "); diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index c51a583..5568769 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -1,4 +1,5 @@ import type { + DatabaseResultSet, DatabaseSchemaItem, DatabaseSchemas, DatabaseTableSchema, @@ -28,9 +29,10 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { }; } - async schemas(): Promise { - const result = await this.query("SELECT * FROM sqlite_schema;"); - + protected getSchemaList( + result: DatabaseResultSet, + schemaName: string + ): DatabaseSchemaItem[] { const tmp: DatabaseSchemaItem[] = []; const rows = result.rows as Array<{ type: string; @@ -44,28 +46,44 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { try { tmp.push({ type: "table", - schemaName: "main", + schemaName, name: row.name, tableSchema: parseCreateTableScript(row.sql), }); } catch { - tmp.push({ type: "table", name: row.name, schemaName: "main" }); + tmp.push({ type: "table", name: row.name, schemaName }); } } else if (row.type === "trigger") { tmp.push({ type: "trigger", name: row.name, tableName: row.tbl_name, - schemaName: "main", + schemaName, }); } else if (row.type === "view") { - tmp.push({ type: "view", name: row.name, schemaName: "main" }); + tmp.push({ type: "view", name: row.name, schemaName }); } } - return { - main: tmp, - }; + return tmp; + } + + async schemas(): Promise { + const databaseList = (await this.query("PRAGMA database_list;")).rows as { + name: string; + }[]; + + const tableListPerDatabase = await this.transaction( + databaseList.map( + (d) => `SELECT * FROM ${this.escapeId(d.name)}.sqlite_schema;` + ) + ); + + return tableListPerDatabase.reduce((a, b, idx) => { + const schemaName = databaseList[idx].name; + a[databaseList[idx].name] = this.getSchemaList(b, schemaName); + return a; + }, {} as DatabaseSchemas); } async trigger(name: string): Promise { @@ -89,7 +107,7 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { schemaName: string, tableName: string ): Promise { - const sql = `SELECT * FROM sqlite_schema WHERE tbl_name = ${escapeSqlValue( + const sql = `SELECT * FROM ${this.escapeId(schemaName)}.sqlite_schema WHERE tbl_name = ${escapeSqlValue( tableName )};`; const result = await this.query(sql); diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index 15ef210..3b141e2 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -69,9 +69,12 @@ function generateKeyFromTab(tab: OpenTabsProps) { return "query-" + window.crypto.randomUUID(); } - if (tab.type === "table") return "table-" + tab.tableName; + if (tab.type === "table") + return "table-" + tab.schemaName + "-" + tab.tableName; if (tab.type === "schema") - return !tab.tableName ? "create-schema" : "schema-" + tab.tableName; + return !tab.tableName + ? "create-schema" + : "schema-" + tab.schemaName + "-" + tab.tableName; if (tab.type === "user") return "user"; return "trigger-" + (tab.name ?? ""); From a7a230dab698cb1ad9202858d866cd69a1d261b6 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 24 Aug 2024 00:26:17 +0700 Subject: [PATCH 05/17] fix use sqlite driver --- src/app/(theme)/embed/sqlite/page-client.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(theme)/embed/sqlite/page-client.tsx b/src/app/(theme)/embed/sqlite/page-client.tsx index db8cc50..63c1bbe 100644 --- a/src/app/(theme)/embed/sqlite/page-client.tsx +++ b/src/app/(theme)/embed/sqlite/page-client.tsx @@ -1,12 +1,12 @@ "use client"; import { Studio } from "@/components/gui/studio"; -import IframeDriver from "@/drivers/iframe-driver"; +import { IframeSQLiteDriver } from "@/drivers/iframe-driver"; import { useSearchParams } from "next/navigation"; import { useEffect, useMemo } from "react"; export default function EmbedPageClient() { const searchParams = useSearchParams(); - const driver = useMemo(() => new IframeDriver(), []); + const driver = useMemo(() => new IframeSQLiteDriver(), []); useEffect(() => { return driver.listen(); From 630bb8a2dc9d780245c70f7228e3706b1fc8f2c1 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Mon, 26 Aug 2024 08:44:48 +0700 Subject: [PATCH 06/17] rework auto completion --- src/components/gui/sql-editor/index.tsx | 4 +-- src/components/gui/tabs/query-tab.tsx | 6 ++-- src/context/schema-provider.tsx | 41 ++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/components/gui/sql-editor/index.tsx b/src/components/gui/sql-editor/index.tsx index 5d8b52a..febd12e 100644 --- a/src/components/gui/sql-editor/index.tsx +++ b/src/components/gui/sql-editor/index.tsx @@ -7,7 +7,7 @@ import { completionStatus, startCompletion, } from "@codemirror/autocomplete"; -import { sql } from "@codemirror/lang-sql"; +import { sql, SQLNamespace } from "@codemirror/lang-sql"; import { forwardRef, KeyboardEventHandler, useMemo } from "react"; import { defaultKeymap, insertTab } from "@codemirror/commands"; @@ -24,7 +24,7 @@ interface SqlEditorProps { value: string; readOnly?: boolean; onChange?: (value: string) => void; - schema?: Record; + schema?: SQLNamespace; onKeyDown?: KeyboardEventHandler; fontSize?: number; onFontSizeChanged?: (fontSize: number) => void; diff --git a/src/components/gui/tabs/query-tab.tsx b/src/components/gui/tabs/query-tab.tsx index f31bc21..329fc25 100644 --- a/src/components/gui/tabs/query-tab.tsx +++ b/src/components/gui/tabs/query-tab.tsx @@ -20,7 +20,6 @@ import { KEY_BINDING } from "@/lib/key-matcher"; import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { selectStatementFromPosition } from "@/drivers/sqlite/sql-helper"; import QueryProgressLog from "../query-progress-log"; -import { useAutoComplete } from "@/context/auto-complete-provider"; import { useDatabaseDriver } from "@/context/driver-provider"; import { MultipleQueryProgress, @@ -56,9 +55,8 @@ export default function QueryWindow({ initialSavedKey, initialNamespace, }: QueryWindowProps) { - const { schema } = useAutoComplete(); const { databaseDriver, docDriver } = useDatabaseDriver(); - const { refresh: refreshSchema } = useSchema(); + const { refresh: refreshSchema, autoCompleteSchema } = useSchema(); const [code, setCode] = useState(initialCode ?? ""); const editorRef = useRef(null); @@ -231,7 +229,7 @@ export default function QueryWindow({ ref={editorRef} value={code} onChange={setCode} - schema={schema} + schema={autoCompleteSchema} fontSize={fontSize} onFontSizeChanged={setFontSize} onCursorChange={(_, line, col) => { diff --git a/src/context/schema-provider.tsx b/src/context/schema-provider.tsx index fc28302..688cc5d 100644 --- a/src/context/schema-provider.tsx +++ b/src/context/schema-provider.tsx @@ -12,13 +12,17 @@ import { DatabaseSchemaItem, DatabaseSchemas } from "@/drivers/base-driver"; import { useDatabaseDriver } from "./driver-provider"; import { useAutoComplete } from "./auto-complete-provider"; +type AutoCompletionSchema = Record | string[]>; + const SchemaContext = createContext<{ schema: DatabaseSchemas; currentSchema: DatabaseSchemaItem[]; + autoCompleteSchema: AutoCompletionSchema; currentSchemaName: string; refresh: () => void; }>({ schema: {}, + autoCompleteSchema: {}, currentSchema: [], currentSchemaName: "", refresh: () => { @@ -26,6 +30,35 @@ const SchemaContext = createContext<{ }, }); +function generateAutoCompleteFromSchemaItems( + items?: DatabaseSchemaItem[] +): Record { + if (!items) return {}; + + return items + .filter((x) => x.type === "table" || x.type === "view") + .reduce( + (a, b) => { + a[b.name] = (b.tableSchema?.columns ?? []).map((c) => c.name); + return a; + }, + {} as Record + ); +} + +function generateAutoComplete( + currentSchemaName: string, + schema: DatabaseSchemas +) { + return { + ...generateAutoCompleteFromSchemaItems(schema[currentSchemaName]), + ...Object.entries(schema).reduce((a, [schemaName, tableList]) => { + a[schemaName] = generateAutoCompleteFromSchemaItems(tableList); + return a; + }, {} as AutoCompletionSchema), + }; +} + export function useSchema() { return useContext(SchemaContext); } @@ -87,7 +120,13 @@ export function SchemaProvider({ children }: Readonly) { }, [fetchSchema]); const props = useMemo(() => { - return { schema, currentSchema, currentSchemaName, refresh: fetchSchema }; + return { + schema, + currentSchema, + currentSchemaName, + refresh: fetchSchema, + autoCompleteSchema: generateAutoComplete(currentSchemaName, schema), + }; }, [schema, fetchSchema, currentSchema, currentSchemaName]); if (error || loading) { From 52632fb5390f7ead89f8ed4ed939dc271f1b3313 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Mon, 2 Sep 2024 09:50:14 +0700 Subject: [PATCH 07/17] add trigger with schema --- src/components/gui/schema-sidebar-list.tsx | 1 + src/components/gui/tabs/trigger-tab.tsx | 12 +++++++++--- src/drivers/base-driver.ts | 5 ++++- src/drivers/mysql/mysql-driver.ts | 1 + src/drivers/sqlite-base-driver.ts | 7 +++++-- src/messages/open-tab.tsx | 3 ++- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index de02e85..4480b3c 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -181,6 +181,7 @@ export default function SchemaList({ search }: Readonly) { } else if (item.data.type === "trigger") { openTab({ type: "trigger", + schemaName: item.data.schemaName, name: item.name, }); } diff --git a/src/components/gui/tabs/trigger-tab.tsx b/src/components/gui/tabs/trigger-tab.tsx index c2b95ba..bcf8aaa 100644 --- a/src/components/gui/tabs/trigger-tab.tsx +++ b/src/components/gui/tabs/trigger-tab.tsx @@ -13,19 +13,25 @@ import SqlEditor from "../sql-editor"; import TableCombobox from "../table-combobox/TableCombobox"; import { noop } from "@/lib/utils"; -export default function TriggerTab({ name }: { name: string }) { +export default function TriggerTab({ + schemaName, + name, +}: { + schemaName: string; + name: string; +}) { const { databaseDriver } = useDatabaseDriver(); const [trigger, setTrigger] = useState(); const [error, setError] = useState(); useEffect(() => { databaseDriver - .trigger(name) + .trigger(schemaName, name) .then(setTrigger) .catch((e: Error) => { setError(e.message); }); - }, [databaseDriver, name]); + }, [databaseDriver, schemaName, name]); if (error) { return
{error}
; diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index b8d84d6..78d072f 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -222,7 +222,10 @@ export abstract class BaseDriver { tableName: string ): Promise; - abstract trigger(name: string): Promise; + abstract trigger( + schemaName: string, + name: string + ): Promise; abstract findFirst( schemaName: string, diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index 626ee7f..d546b72 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -49,6 +49,7 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { defaultSchema: "", optionalSchema: false, supportBigInt: false, + mismatchDetection: false, }; } diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index 5998797..61cab24 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -87,9 +87,12 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { }, {} as DatabaseSchemas); } - async trigger(name: string): Promise { + async trigger( + schemaName: string, + name: string + ): Promise { const result = await this.query( - `SELECT * FROM sqlite_schema WHERE "type"='trigger' AND name=${escapeSqlValue( + `SELECT * FROM ${this.escapeId(schemaName)}.sqlite_schema WHERE "type"='trigger' AND name=${escapeSqlValue( name )};` ); diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index 3b141e2..7bc4327 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -42,6 +42,7 @@ interface OpenUserTab { interface OpenTriggerTab { type: "trigger"; + schemaName: string; tableName?: string; name?: string; } @@ -124,7 +125,7 @@ function generateComponent(tab: OpenTabsProps, title: string) { ); if (tab.type === "user") return ; - return ; + return ; } export function receiveOpenTabMessage({ From bb1b91af9c5b64aad6781830f0adc2b7da6d29a7 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Mon, 2 Sep 2024 10:21:36 +0700 Subject: [PATCH 08/17] flat the schema if there is only one schema --- src/components/gui/database-gui.tsx | 6 ++-- src/components/gui/schema-sidebar-list.tsx | 36 ++++++++++++------- .../gui/table-cell/generic-cell.tsx | 20 +++++------ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/components/gui/database-gui.tsx b/src/components/gui/database-gui.tsx index b8678df..16b6544 100644 --- a/src/components/gui/database-gui.tsx +++ b/src/components/gui/database-gui.tsx @@ -23,6 +23,7 @@ import SettingSidebar from "./sidebar/setting-sidebar"; import { useDatabaseDriver } from "@/context/driver-provider"; import SavedDocTab from "./sidebar/saved-doc-tab"; +import { useSchema } from "@/context/schema-provider"; export default function DatabaseGui() { const DEFAULT_WIDTH = 300; @@ -35,6 +36,7 @@ export default function DatabaseGui() { const { collaborationDriver, docDriver } = useDatabaseDriver(); const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const { currentSchemaName } = useSchema(); const [tabs, setTabs] = useState(() => [ { title: "Query", @@ -122,11 +124,11 @@ export default function DatabaseGui() { { text: "New Table", onClick: () => { - openTab({ type: "schema", schemaName: "main" }); + openTab({ type: "schema", schemaName: currentSchemaName }); }, }, ]; - }, []); + }, [currentSchemaName]); return (
diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index 4480b3c..37b7853 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -87,6 +87,14 @@ function groupByFtsTable(items: ListViewItem[]) { return items.filter((item) => !excludes.has(item.data.name)); } +function flattenSchemaGroup( + schemaGroup: ListViewItem[] +): ListViewItem[] { + console.log(schemaGroup); + if (schemaGroup.length === 1) return schemaGroup[0].children ?? []; + return schemaGroup; +} + export default function SchemaList({ search }: Readonly) { const [selected, setSelected] = useState(""); const [collapsed, setCollapsed] = useState(new Set()); @@ -138,18 +146,20 @@ export default function SchemaList({ search }: Readonly) { [refresh] ); - const filteredSchema = useMemo(() => { - return Object.entries(schema).map(([s, tables]) => { - return { - data: {}, - icon: LucideDatabase, - name: s, - key: s.toString(), - children: groupByFtsTable( - groupTriggerByTable(prepareListViewItem(tables)) - ), - } as ListViewItem; - }); + const listViewItems = useMemo(() => { + return flattenSchemaGroup( + Object.entries(schema).map(([s, tables]) => { + return { + data: {}, + icon: LucideDatabase, + name: s, + key: s.toString(), + children: groupByFtsTable( + groupTriggerByTable(prepareListViewItem(tables)) + ), + } as ListViewItem; + }) + ); }, [schema]); const filterCallback = useCallback( @@ -165,7 +175,7 @@ export default function SchemaList({ search }: Readonly) { full filter={filterCallback} highlight={search} - items={filteredSchema} + items={listViewItems} collapsedKeys={collapsed} onCollapsedChange={setCollapsed} onContextMenu={(item) => prepareContextMenu(item?.data)} diff --git a/src/components/gui/table-cell/generic-cell.tsx b/src/components/gui/table-cell/generic-cell.tsx index ab061d0..93668e7 100644 --- a/src/components/gui/table-cell/generic-cell.tsx +++ b/src/components/gui/table-cell/generic-cell.tsx @@ -12,10 +12,16 @@ import { import { DatabaseResultSet, DatabaseValue, + describeTableColumnType, TableColumnDataType, } from "@/drivers/base-driver"; import { useDatabaseDriver } from "@/context/driver-provider"; import { convertDatabaseValueToString } from "@/drivers/sqlite/sql-helper"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; interface TableCellProps { align?: "left" | "right"; @@ -30,27 +36,21 @@ interface TableCellProps { } interface SneakpeakProps { - fkSchemaName: string; fkTableName: string; fkColumnName: string; value: DatabaseValue; } -function SnippetRow({ - fkSchemaName, - fkTableName, - fkColumnName, - value, -}: SneakpeakProps) { +function SnippetRow({ fkTableName, fkColumnName, value }: SneakpeakProps) { const { databaseDriver } = useDatabaseDriver(); const [data, setData] = useState(); useEffect(() => { databaseDriver - .findFirst(fkSchemaName, fkTableName, { [fkColumnName]: value }) + .findFirst("main", fkTableName, { [fkColumnName]: value }) .then(setData) .catch(console.error); - }, [databaseDriver, fkSchemaName, fkTableName, fkColumnName, value]); + }, [databaseDriver, fkTableName, fkColumnName, value]); if (!data) { return ( @@ -178,6 +178,7 @@ function BlobCellValue({ export default function GenericCell({ value, + valueType, onFocus, isChanged, focus, @@ -207,7 +208,6 @@ export default function GenericCell({ return (
Date: Mon, 2 Sep 2024 19:47:50 +0700 Subject: [PATCH 09/17] add schema list on table editor --- src/components/gui/schema-editor/index.tsx | 48 ++++-------- .../schema-editor-column-list.tsx | 3 +- .../schema-editor-constraint-list.tsx | 7 +- .../gui/schema-editor/schema-name-select.tsx | 78 +++++++++++++++++++ .../gui/schema-editor/schema-save-dialog.tsx | 16 ++-- src/components/gui/schema-sidebar-list.tsx | 42 ++++++---- src/components/gui/schema-sidebar.tsx | 1 + src/components/gui/tabs/schema-editor-tab.tsx | 25 +++--- src/components/lib/sql-generate.schema.ts | 14 ++-- src/drivers/base-driver.ts | 24 ++++++ src/drivers/mysql/mysql-driver.ts | 4 + src/drivers/sqlite-base-driver.ts | 6 ++ src/messages/open-tab.tsx | 2 +- 13 files changed, 191 insertions(+), 79 deletions(-) create mode 100644 src/components/gui/schema-editor/schema-name-select.tsx diff --git a/src/components/gui/schema-editor/index.tsx b/src/components/gui/schema-editor/index.tsx index dc5a3d1..cfc4f83 100644 --- a/src/components/gui/schema-editor/index.tsx +++ b/src/components/gui/schema-editor/index.tsx @@ -4,45 +4,22 @@ import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; import { Button, buttonVariants } from "../../ui/button"; import SchemaEditorColumnList from "./schema-editor-column-list"; import { Input } from "../../ui/input"; -import generateSqlSchemaChange, { - checkSchemaChange, -} from "@/components/lib/sql-generate.schema"; -import { - DatabaseTableColumn, - DatabaseTableColumnConstraint, -} from "@/drivers/base-driver"; +import { checkSchemaChange } from "@/components/lib/sql-generate.schema"; import SchemaEditorConstraintList from "./schema-editor-constraint-list"; import { ColumnsProvider } from "./column-provider"; import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; import CodePreview from "../code-preview"; import { toast } from "sonner"; - -export interface DatabaseTableColumnChange { - old: DatabaseTableColumn | null; - new: DatabaseTableColumn | null; -} - -export interface DatabaseTableConstraintChange { - id: string; - old: DatabaseTableColumnConstraint | null; - new: DatabaseTableColumnConstraint | null; -} - -export interface DatabaseTableSchemaChange { - name: { - old?: string; - new?: string; - }; - columns: DatabaseTableColumnChange[]; - constraints: DatabaseTableConstraintChange[]; - createScript?: string; -} +import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; +import { useDatabaseDriver } from "@/context/driver-provider"; +import SchemaNameSelect from "./schema-name-select"; interface Props { onSave: () => void; onDiscard: () => void; value: DatabaseTableSchemaChange; onChange: Dispatch>; + isCreate?: boolean; } export default function SchemaEditor({ @@ -50,7 +27,9 @@ export default function SchemaEditor({ onChange, onSave, onDiscard, + isCreate, }: Readonly) { + const { databaseDriver } = useDatabaseDriver(); const isCreateScript = value.name.old === ""; const onAddColumn = useCallback(() => { @@ -84,8 +63,8 @@ export default function SchemaEditor({ const hasChange = checkSchemaChange(value); const previewScript = useMemo(() => { - return generateSqlSchemaChange(value).join("\n"); - }, [value]); + return databaseDriver.createUpdateTableSchema(value).join("\n"); + }, [value, databaseDriver]); return (
@@ -174,7 +153,14 @@ export default function SchemaEditor({
-
Name
+ { + onChange({ ...value, schemaName: selectedSchema }); + }} + /> + ● void; + readonly?: boolean; +} + +export default function SchemaNameSelect({ + onChange, + value, + readonly, +}: SchemaNameSelectProps) { + const { schema } = useSchema(); + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + No matched schema + + {Object.keys(schema).map((s) => ( + { + onChange(s); + setOpen(false); + }} + > + + {s} + + ))} + + + + + + ); +} diff --git a/src/components/gui/schema-editor/schema-save-dialog.tsx b/src/components/gui/schema-editor/schema-save-dialog.tsx index e7f6722..f2ec009 100644 --- a/src/components/gui/schema-editor/schema-save-dialog.tsx +++ b/src/components/gui/schema-editor/schema-save-dialog.tsx @@ -12,25 +12,23 @@ import { LucideSave, LucideTableProperties, } from "lucide-react"; -import { DatabaseTableSchemaChange } from "."; import { useTabsContext } from "../windows-tab"; import { useDatabaseDriver } from "@/context/driver-provider"; import { useSchema } from "@/context/schema-provider"; import { useCallback, useState } from "react"; import SchemaEditorTab from "../tabs/schema-editor-tab"; +import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; export default function SchemaSaveDialog({ - schemaName, schema, previewScript, onClose, fetchTable, }: { - schemaName: string; schema: DatabaseTableSchemaChange; previewScript: string[]; onClose: () => void; - fetchTable: (tableName: string) => Promise; + fetchTable: (schemeName: string, tableName: string) => Promise; }) { const { databaseDriver } = useDatabaseDriver(); const { refresh: refreshSchema } = useSchema(); @@ -49,7 +47,7 @@ export default function SchemaSaveDialog({ component: ( ), key: "_schema_" + schema.name.new, @@ -57,8 +55,11 @@ export default function SchemaSaveDialog({ title: "Edit " + schema.name.new, icon: LucideTableProperties, }); - } else if (schema.name.old) { - fetchTable(schema.name?.new || schema.name?.old || "").then(onClose); + } else if (schema.name.old && schema.schemaName) { + fetchTable( + schema.schemaName, + schema.name?.new || schema.name?.old || "" + ).then(onClose); } }) .catch((err) => setErrorMessage((err as Error).message)) @@ -67,7 +68,6 @@ export default function SchemaSaveDialog({ }); }, [ onClose, - schemaName, databaseDriver, schema, fetchTable, diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index 37b7853..9d626a1 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -5,6 +5,7 @@ import { openTab } from "@/messages/open-tab"; import { DatabaseSchemaItem } from "@/drivers/base-driver"; import { useSchema } from "@/context/schema-provider"; import { ListView, ListViewItem } from "../listview"; +import { useDatabaseDriver } from "@/context/driver-provider"; interface SchemaListProps { search: string; @@ -90,16 +91,19 @@ function groupByFtsTable(items: ListViewItem[]) { function flattenSchemaGroup( schemaGroup: ListViewItem[] ): ListViewItem[] { - console.log(schemaGroup); if (schemaGroup.length === 1) return schemaGroup[0].children ?? []; return schemaGroup; } export default function SchemaList({ search }: Readonly) { + const { databaseDriver } = useDatabaseDriver(); const [selected, setSelected] = useState(""); - const [collapsed, setCollapsed] = useState(new Set()); const { refresh, schema } = useSchema(); + const [collapsed, setCollapsed] = useState(() => { + return new Set(); + }); + useEffect(() => { setSelected(""); }, [setSelected, search]); @@ -147,20 +151,26 @@ export default function SchemaList({ search }: Readonly) { ); const listViewItems = useMemo(() => { - return flattenSchemaGroup( - Object.entries(schema).map(([s, tables]) => { - return { - data: {}, - icon: LucideDatabase, - name: s, - key: s.toString(), - children: groupByFtsTable( - groupTriggerByTable(prepareListViewItem(tables)) - ), - } as ListViewItem; - }) - ); - }, [schema]); + const r = Object.entries(schema).map(([s, tables]) => { + return { + data: {}, + icon: LucideDatabase, + name: s, + key: s.toString(), + children: groupByFtsTable( + groupTriggerByTable(prepareListViewItem(tables)) + ), + } as ListViewItem; + }); + + if (databaseDriver.getFlags().optionalSchema) { + // For SQLite, the default schema is main and + // it is optional. + return flattenSchemaGroup(r); + } + return r; + }, [schema, databaseDriver]); + console.log(listViewItems, schema); const filterCallback = useCallback( (item: ListViewItem) => { diff --git a/src/components/gui/schema-sidebar.tsx b/src/components/gui/schema-sidebar.tsx index b8bcadb..0e26bb8 100644 --- a/src/components/gui/schema-sidebar.tsx +++ b/src/components/gui/schema-sidebar.tsx @@ -7,6 +7,7 @@ import { openTab } from "@/messages/open-tab"; export default function SchemaView() { const [search, setSearch] = useState(""); + const onNewTable = useCallback(() => { openTab({ type: "schema", diff --git a/src/components/gui/tabs/schema-editor-tab.tsx b/src/components/gui/tabs/schema-editor-tab.tsx index 017ffea..d12e7d5 100644 --- a/src/components/gui/tabs/schema-editor-tab.tsx +++ b/src/components/gui/tabs/schema-editor-tab.tsx @@ -1,15 +1,13 @@ import OpacityLoading from "@/components/gui/loading-opacity"; -import SchemaEditor, { - DatabaseTableSchemaChange, -} from "@/components/gui/schema-editor"; -import generateSqlSchemaChange from "@/components/lib/sql-generate.schema"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDatabaseDriver } from "@/context/driver-provider"; import SchemaSaveDialog from "../schema-editor/schema-save-dialog"; +import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; +import SchemaEditor from "../schema-editor"; interface SchemaEditorTabProps { tableName?: string; - schemaName: string; + schemaName?: string; } const EMPTY_SCHEMA: DatabaseTableSchemaChange = { @@ -32,11 +30,12 @@ export default function SchemaEditorTab({ const [isSaving, setIsSaving] = useState(false); const fetchTable = useCallback( - async (name: string) => { + async (schemaName: string, name: string) => { databaseDriver .tableSchema(schemaName, name) .then((schema) => { setSchema({ + schemaName, name: { old: schema.tableName, new: schema.tableName, @@ -56,18 +55,18 @@ export default function SchemaEditorTab({ .catch(console.error) .finally(() => setLoading(false)); }, - [schemaName, databaseDriver, setSchema] + [databaseDriver, setSchema] ); useEffect(() => { - if (tableName) { - fetchTable(tableName).then().catch(console.error); + if (tableName && schemaName) { + fetchTable(schemaName, tableName).then().catch(console.error); } - }, [fetchTable, tableName]); + }, [fetchTable, schemaName, tableName]); const previewScript = useMemo(() => { - return generateSqlSchemaChange(schema); - }, [schema]); + return databaseDriver.createUpdateTableSchema(schema); + }, [schema, databaseDriver]); const onSaveToggle = useCallback( () => setIsSaving((prev) => !prev), @@ -104,7 +103,6 @@ export default function SchemaEditorTab({ <> {isSaving && ( ); diff --git a/src/components/lib/sql-generate.schema.ts b/src/components/lib/sql-generate.schema.ts index 010d3c1..0434ea9 100644 --- a/src/components/lib/sql-generate.schema.ts +++ b/src/components/lib/sql-generate.schema.ts @@ -1,12 +1,10 @@ -import { - DatabaseTableColumnChange, - DatabaseTableSchemaChange, -} from "@/components/gui/schema-editor"; import { escapeIdentity, escapeSqlValue } from "@/drivers/sqlite/sql-helper"; import deepEqual from "deep-equal"; import { DatabaseTableColumn, + DatabaseTableColumnChange, DatabaseTableColumnConstraint, + DatabaseTableSchemaChange, } from "@/drivers/base-driver"; export function checkSchemaColumnChange(change: DatabaseTableColumnChange) { @@ -170,18 +168,20 @@ export default function generateSqlSchemaChange( if (!isCreateScript) { if (change.name.new !== change.name.old) { - lines.push("RENAME TO " + escapeIdentity(change.name.new ?? "")); + lines.push( + `RENAME TO ${escapeIdentity(change.schemaName ?? "main")}.${escapeIdentity(change.name.new ?? "")}` + ); } } if (isCreateScript) { return [ - `CREATE TABLE ${escapeIdentity( + `CREATE TABLE ${escapeIdentity(change.schemaName ?? "main")}.${escapeIdentity( change.name.new || "no_table_name" )}(\n${lines.map((line) => " " + line).join(",\n")}\n)`, ]; } else { - const alter = `ALTER TABLE ${escapeIdentity(change.name.old ?? "")} `; + const alter = `ALTER TABLE ${escapeIdentity(change.schemaName ?? "main")}.${escapeIdentity(change.name.old ?? "")} `; return lines.map((line) => alter + line); } } diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index 78d072f..a5cab68 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -202,6 +202,28 @@ export interface DriverFlags { mismatchDetection: boolean; } +export interface DatabaseTableColumnChange { + old: DatabaseTableColumn | null; + new: DatabaseTableColumn | null; +} + +export interface DatabaseTableConstraintChange { + id: string; + old: DatabaseTableColumnConstraint | null; + new: DatabaseTableColumnConstraint | null; +} + +export interface DatabaseTableSchemaChange { + schemaName?: string; + name: { + old?: string; + new?: string; + }; + columns: DatabaseTableColumnChange[]; + constraints: DatabaseTableConstraintChange[]; + createScript?: string; +} + export abstract class BaseDriver { // Flags abstract getFlags(): DriverFlags; @@ -248,4 +270,6 @@ export abstract class BaseDriver { // if the operation is unsafe validateSchema?: DatabaseTableSchema ): Promise; + + abstract createUpdateTableSchema(change: DatabaseTableSchemaChange): string[]; } diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index d546b72..8be3c2c 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -144,4 +144,8 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { trigger(): Promise { throw new Error("Not implemented"); } + + createUpdateTableSchema(): string[] { + throw new Error("Not implemented"); + } } diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index 61cab24..56331f1 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -3,6 +3,7 @@ import type { DatabaseSchemaItem, DatabaseSchemas, DatabaseTableSchema, + DatabaseTableSchemaChange, DatabaseTriggerSchema, DriverFlags, } from "./base-driver"; @@ -11,6 +12,7 @@ import { escapeSqlValue } from "@/drivers/sqlite/sql-helper"; import { parseCreateTableScript } from "@/drivers/sqlite/sql-parse-table"; import { parseCreateTriggerScript } from "@/drivers/sqlite/sql-parse-trigger"; import CommonSQLImplement from "./common-sql-imp"; +import generateSqlSchemaChange from "@/components/lib/sql-generate.schema"; export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { escapeId(id: string) { @@ -133,4 +135,8 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { throw new Error("Unexpected error while parsing"); } } + + createUpdateTableSchema(change: DatabaseTableSchemaChange): string[] { + return generateSqlSchemaChange(change); + } } diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index 7bc4327..c585ad9 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -32,7 +32,7 @@ interface OpenQueryTab { interface OpenTableSchemaTab { type: "schema"; - schemaName: string; + schemaName?: string; tableName?: string; } From 6011f1dbf6828ec5ad387de5cfdc5a555ae97503 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Mon, 2 Sep 2024 20:09:57 +0700 Subject: [PATCH 10/17] auto select schema name based on where create table is selected --- src/components/gui/schema-editor/index.tsx | 55 ++++++++++--------- .../gui/schema-editor/schema-name-select.tsx | 2 +- src/components/gui/schema-sidebar-list.tsx | 8 +-- src/components/gui/tabs/schema-editor-tab.tsx | 6 +- src/drivers/base-driver.ts | 2 +- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/components/gui/schema-editor/index.tsx b/src/components/gui/schema-editor/index.tsx index cfc4f83..d55503c 100644 --- a/src/components/gui/schema-editor/index.tsx +++ b/src/components/gui/schema-editor/index.tsx @@ -19,7 +19,6 @@ interface Props { onDiscard: () => void; value: DatabaseTableSchemaChange; onChange: Dispatch>; - isCreate?: boolean; } export default function SchemaEditor({ @@ -27,7 +26,6 @@ export default function SchemaEditor({ onChange, onSave, onDiscard, - isCreate, }: Readonly) { const { databaseDriver } = useDatabaseDriver(); const isCreateScript = value.name.old === ""; @@ -73,7 +71,7 @@ export default function SchemaEditor({
-
- { - onChange({ ...value, schemaName: selectedSchema }); - }} - /> - ● - { - onChange({ - ...value, - name: { - ...value.name, - new: e.currentTarget.value, - }, - }); - }} - className="w-[200px]" - /> +
+
+
Table Name
+ { + onChange({ + ...value, + name: { + ...value.name, + new: e.currentTarget.value, + }, + }); + }} + className="w-[200px]" + /> +
+
+
Schema
+ { + onChange({ ...value, schemaName: selectedSchema }); + }} + /> +
diff --git a/src/components/gui/schema-editor/schema-name-select.tsx b/src/components/gui/schema-editor/schema-name-select.tsx index da30a4f..defada2 100644 --- a/src/components/gui/schema-editor/schema-name-select.tsx +++ b/src/components/gui/schema-editor/schema-name-select.tsx @@ -40,7 +40,7 @@ export default function SchemaNameSelect({ className="w-[200px] justify-between" disabled={readonly} > - {value ?? "Select schema"} + {value || "Select schema"} diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index 9d626a1..6751262 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -98,7 +98,7 @@ function flattenSchemaGroup( export default function SchemaList({ search }: Readonly) { const { databaseDriver } = useDatabaseDriver(); const [selected, setSelected] = useState(""); - const { refresh, schema } = useSchema(); + const { refresh, schema, currentSchemaName } = useSchema(); const [collapsed, setCollapsed] = useState(() => { return new Set(); @@ -127,7 +127,7 @@ export default function SchemaList({ search }: Readonly) { onClick: () => { openTab({ type: "schema", - schemaName: item?.schemaName ?? "", + schemaName: item?.schemaName ?? currentSchemaName, }); }, }, @@ -147,13 +147,13 @@ export default function SchemaList({ search }: Readonly) { { title: "Refresh", onClick: () => refresh() }, ].filter(Boolean) as OpenContextMenuList; }, - [refresh] + [refresh, currentSchemaName] ); const listViewItems = useMemo(() => { const r = Object.entries(schema).map(([s, tables]) => { return { - data: {}, + data: { type: "schema", schemaName: s }, icon: LucideDatabase, name: s, key: s.toString(), diff --git a/src/components/gui/tabs/schema-editor-tab.tsx b/src/components/gui/tabs/schema-editor-tab.tsx index d12e7d5..038dcb1 100644 --- a/src/components/gui/tabs/schema-editor-tab.tsx +++ b/src/components/gui/tabs/schema-editor-tab.tsx @@ -25,7 +25,10 @@ export default function SchemaEditorTab({ tableName, }: Readonly) { const { databaseDriver } = useDatabaseDriver(); - const [schema, setSchema] = useState(EMPTY_SCHEMA); + const [schema, setSchema] = useState({ + ...EMPTY_SCHEMA, + schemaName, + }); const [loading, setLoading] = useState(!!tableName); const [isSaving, setIsSaving] = useState(false); @@ -115,7 +118,6 @@ export default function SchemaEditorTab({ onChange={setSchema} onSave={onSaveToggle} onDiscard={onDiscard} - isCreate={!tableName && !schemaName} /> ); diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index a5cab68..4486b08 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -72,7 +72,7 @@ export type DatabaseValue = T | undefined | null; export type DatabaseSchemas = Record; export interface DatabaseSchemaItem { - type: "table" | "trigger" | "view"; + type: "table" | "trigger" | "view" | "schema"; name: string; schemaName: string; tableName?: string; From 8cb664424bfab1a0f363f197136a920a79a8aae1 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Mon, 2 Sep 2024 21:08:58 +0700 Subject: [PATCH 11/17] add disable edit create table flag --- src/app/(theme)/embed/mysql/page-client.tsx | 22 ++++++++++ src/app/(theme)/embed/mysql/page.tsx | 46 ++++++++++++++------- src/components/gui/database-gui.tsx | 21 ++++++---- src/components/gui/schema-sidebar-list.tsx | 10 +++-- src/drivers/base-driver.ts | 1 + src/drivers/mysql/mysql-driver.ts | 1 + src/drivers/sqlite-base-driver.ts | 1 + 7 files changed, 73 insertions(+), 29 deletions(-) create mode 100644 src/app/(theme)/embed/mysql/page-client.tsx diff --git a/src/app/(theme)/embed/mysql/page-client.tsx b/src/app/(theme)/embed/mysql/page-client.tsx new file mode 100644 index 0000000..f1c5887 --- /dev/null +++ b/src/app/(theme)/embed/mysql/page-client.tsx @@ -0,0 +1,22 @@ +"use client"; +import { Studio } from "@/components/gui/studio"; +import { IframeMySQLDriver } from "@/drivers/iframe-driver"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useMemo } from "react"; + +export default function EmbedPageClient() { + const searchParams = useSearchParams(); + const driver = useMemo(() => new IframeMySQLDriver(), []); + + useEffect(() => { + return driver.listen(); + }, [driver]); + + return ( + + ); +} diff --git a/src/app/(theme)/embed/mysql/page.tsx b/src/app/(theme)/embed/mysql/page.tsx index 6952204..abe190a 100644 --- a/src/app/(theme)/embed/mysql/page.tsx +++ b/src/app/(theme)/embed/mysql/page.tsx @@ -1,23 +1,37 @@ -"use client"; +import ThemeLayout from "../../theme_layout"; +import EmbedPageClient from "./page-client"; -import MyStudio from "@/components/my-studio"; -import { IframeMySQLDriver } from "@/drivers/iframe-driver"; -import { useSearchParams } from "next/navigation"; -import { useEffect, useMemo } from "react"; +export default async function EmbedPage(props: { + searchParams: { + theme?: string; + disableThemeToggle?: string; + [key: string]: any; + }; +}) { + let overrideTheme: "dark" | "light" | undefined = undefined; + const disableToggle = props.searchParams.disableThemeToggle === "1"; -export default function EmbedPageClient() { - const searchParams = useSearchParams(); - const driver = useMemo(() => new IframeMySQLDriver(), []); + if (props.searchParams.theme) { + overrideTheme = props.searchParams.theme === "dark" ? "dark" : "light"; + } - useEffect(() => { - return driver.listen(); - }, [driver]); + const overrideThemeVariables: Record = {}; + + for (const key in props.searchParams) { + if (!key.startsWith("themeVariables[")) { + continue; + } + + overrideThemeVariables[key.slice(15, -1)] = props.searchParams[key]; + } return ( - + + + ); } diff --git a/src/components/gui/database-gui.tsx b/src/components/gui/database-gui.tsx index 16b6544..b5670cf 100644 --- a/src/components/gui/database-gui.tsx +++ b/src/components/gui/database-gui.tsx @@ -34,7 +34,8 @@ export default function DatabaseGui() { setDefaultWidthPercentage((DEFAULT_WIDTH / window.innerWidth) * 100); }, []); - const { collaborationDriver, docDriver } = useDatabaseDriver(); + const { databaseDriver, collaborationDriver, docDriver } = + useDatabaseDriver(); const [selectedTabIndex, setSelectedTabIndex] = useState(0); const { currentSchemaName } = useSchema(); const [tabs, setTabs] = useState(() => [ @@ -121,14 +122,16 @@ export default function DatabaseGui() { openTab({ type: "query" }); }, }, - { - text: "New Table", - onClick: () => { - openTab({ type: "schema", schemaName: currentSchemaName }); - }, - }, - ]; - }, [currentSchemaName]); + databaseDriver.getFlags().supportCreateUpdateTable + ? { + text: "New Table", + onClick: () => { + openTab({ type: "schema", schemaName: currentSchemaName }); + }, + } + : undefined, + ].filter(Boolean) as { text: string; onClick: () => void }[]; + }, [currentSchemaName, databaseDriver]); return (
diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index 6751262..b45d5f4 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -122,7 +122,7 @@ export default function SchemaList({ search }: Readonly) { }, }, { separator: true }, - { + databaseDriver.getFlags().supportCreateUpdateTable && { title: "Create New Table", onClick: () => { openTab({ @@ -131,7 +131,7 @@ export default function SchemaList({ search }: Readonly) { }); }, }, - isTable + isTable && databaseDriver.getFlags().supportCreateUpdateTable ? { title: "Edit Table", onClick: () => { @@ -143,11 +143,13 @@ export default function SchemaList({ search }: Readonly) { }, } : undefined, - { separator: true }, + databaseDriver.getFlags().supportCreateUpdateTable + ? { separator: true } + : undefined, { title: "Refresh", onClick: () => refresh() }, ].filter(Boolean) as OpenContextMenuList; }, - [refresh, currentSchemaName] + [refresh, databaseDriver, currentSchemaName] ); const listViewItems = useMemo(() => { diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index 4486b08..e0014f1 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -199,6 +199,7 @@ export interface DriverFlags { defaultSchema: string; optionalSchema: boolean; supportBigInt: boolean; + supportCreateUpdateTable: boolean; mismatchDetection: boolean; } diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index 8be3c2c..712d070 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -50,6 +50,7 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { optionalSchema: false, supportBigInt: false, mismatchDetection: false, + supportCreateUpdateTable: false, }; } diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index 56331f1..999e0da 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -29,6 +29,7 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { defaultSchema: "main", optionalSchema: true, mismatchDetection: false, + supportCreateUpdateTable: true, }; } From b8c70f0262e750b98dd29a6e55ca26f426d38c96 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 3 Sep 2024 08:12:41 +0700 Subject: [PATCH 12/17] everything should use schema name instead of "main" schema --- .../gui/schema-editor/column-fk-popup.tsx | 5 +- src/components/gui/schema-editor/index.tsx | 2 + .../schema-editor-column-list.tsx | 15 +++++- .../schema-editor-constraint-list.tsx | 20 ++++++-- src/components/gui/schema-sidebar.tsx | 6 ++- .../gui/table-cell/generic-cell.tsx | 13 +++-- .../table-combobox/TableColumnCombobox.tsx | 2 +- .../gui/table-combobox/TableCombobox.tsx | 6 ++- src/components/gui/tabs/trigger-tab.tsx | 6 ++- src/components/lib/sql-execute-helper.ts | 2 +- src/drivers/base-driver.ts | 1 + src/drivers/sqlite-base-driver.ts | 4 +- src/drivers/sqlite/sql-parse-table.test.ts | 4 +- src/drivers/sqlite/sql-parse-table.ts | 47 ++++++++++++------- 14 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/components/gui/schema-editor/column-fk-popup.tsx b/src/components/gui/schema-editor/column-fk-popup.tsx index 8787cd7..6033eed 100644 --- a/src/components/gui/schema-editor/column-fk-popup.tsx +++ b/src/components/gui/schema-editor/column-fk-popup.tsx @@ -11,9 +11,11 @@ import { Separator } from "../../ui/separator"; export default function ColumnForeignKeyPopup({ constraint, disabled, + schemaName, onChange, }: Readonly<{ constraint: DatabaseForeignKeyClause; + schemaName: string; disabled: boolean; onChange: ColumnChangeEvent; }>) { @@ -33,6 +35,7 @@ export default function ColumnForeignKeyPopup({
{ @@ -64,7 +67,7 @@ export default function ColumnForeignKeyPopup({ }, }); }} - schemaName={"main"} + schemaName={schemaName} tableName={constraint.foreignTableName} />
diff --git a/src/components/gui/schema-editor/index.tsx b/src/components/gui/schema-editor/index.tsx index d55503c..adb4045 100644 --- a/src/components/gui/schema-editor/index.tsx +++ b/src/components/gui/schema-editor/index.tsx @@ -186,9 +186,11 @@ export default function SchemaEditor({ columns={value.columns} onChange={onChange} onAddColumn={onAddColumn} + schemaName={value.schemaName} /> >; }) { const disabled = !!value.old; @@ -198,11 +200,12 @@ function ColumnItem({ /> )} - {column.constraint?.foreignKey && ( + {column.constraint?.foreignKey && schemaName && ( )} @@ -294,10 +297,12 @@ function ColumnItem({ export default function SchemaEditorColumnList({ columns, onChange, + schemaName, onAddColumn, }: Readonly<{ columns: DatabaseTableColumnChange[]; onChange: Dispatch>; + schemaName?: string; onAddColumn: () => void; }>) { const headerStyle = "text-xs p-2 text-left bg-secondary border"; @@ -318,7 +323,13 @@ export default function SchemaEditorColumnList({ {columns.map((col, idx) => ( - + ))} diff --git a/src/components/gui/schema-editor/schema-editor-constraint-list.tsx b/src/components/gui/schema-editor/schema-editor-constraint-list.tsx index 2ebfd9c..0995897 100644 --- a/src/components/gui/schema-editor/schema-editor-constraint-list.tsx +++ b/src/components/gui/schema-editor/schema-editor-constraint-list.tsx @@ -70,13 +70,15 @@ function ColumnForeignKey({ constraint, onChange, disabled, + schemaName, }: Readonly<{ constraint: DatabaseTableColumnConstraint; onChange: ConstraintChangeHandler; disabled?: boolean; + schemaName: string; }>) { const { columns } = useColumnList(); - const { currentSchema } = useSchema(); + const { schema } = useSchema(); const columnMemo = useMemo(() => { return [...new Set(columns.map((c) => c.new?.name ?? c.old?.name ?? ""))]; @@ -86,7 +88,7 @@ function ColumnForeignKey({ const fkTableName = constraint.foreignKey?.foreignTableName; if (fkTableName) { - const fkTableSchema = currentSchema.find( + const fkTableSchema = (schema[schemaName] ?? []).find( (s) => s.type === "table" && s.name === fkTableName ); @@ -96,7 +98,7 @@ function ColumnForeignKey({ } return []; - }, [constraint, currentSchema]); + }, [constraint, schemaName, schema]); const onConstraintChange = useCallback( (newColumn: string[]) => { @@ -137,6 +139,7 @@ function ColumnForeignKey({ >; constraint: DatabaseTableConstraintChange; + schemaName?: string; disabled?: boolean; }>) { const onChangeConstraint = useCallback( @@ -315,12 +320,13 @@ function ColumnItemBody({ const currentConstraint = constraint.new ?? constraint.old; if (!currentConstraint) return null; - if (currentConstraint.foreignKey) { + if (currentConstraint.foreignKey && schemaName) { return ( ); } @@ -362,12 +368,14 @@ function ColumnItem({ constraint, onChange, idx, + schemaName, disabled, }: Readonly<{ constraint: DatabaseTableConstraintChange; onChange: Dispatch>; idx: number; disabled?: boolean; + schemaName?: string; }>) { return ( @@ -375,6 +383,7 @@ function ColumnItem({ constraint={constraint} onChange={onChange} idx={idx} + schemaName={schemaName} disabled={disabled} /> @@ -384,10 +393,12 @@ function ColumnItem({ export default function SchemaEditorConstraintList({ constraints, onChange, + schemaName, disabled, }: Readonly<{ constraints: DatabaseTableConstraintChange[]; onChange: Dispatch>; + schemaName?: string; disabled?: boolean; }>) { const headerClassName = "text-xs p-2 text-left bg-secondary border"; @@ -429,6 +440,7 @@ export default function SchemaEditorConstraintList({ constraint={constraint} onChange={onChange} disabled={disabled} + schemaName={schemaName} /> ); })} diff --git a/src/components/gui/schema-sidebar.tsx b/src/components/gui/schema-sidebar.tsx index 0e26bb8..a95d8b2 100644 --- a/src/components/gui/schema-sidebar.tsx +++ b/src/components/gui/schema-sidebar.tsx @@ -4,16 +4,18 @@ import SchemaList from "./schema-sidebar-list"; import ListButtonItem from "./list-button-item"; import { Separator } from "../ui/separator"; import { openTab } from "@/messages/open-tab"; +import { useSchema } from "@/context/schema-provider"; export default function SchemaView() { const [search, setSearch] = useState(""); + const { currentSchemaName } = useSchema(); const onNewTable = useCallback(() => { openTab({ type: "schema", - schemaName: "main", + schemaName: currentSchemaName, }); - }, []); + }, [currentSchemaName]); return (
diff --git a/src/components/gui/table-cell/generic-cell.tsx b/src/components/gui/table-cell/generic-cell.tsx index 93668e7..e65737a 100644 --- a/src/components/gui/table-cell/generic-cell.tsx +++ b/src/components/gui/table-cell/generic-cell.tsx @@ -36,21 +36,27 @@ interface TableCellProps { } interface SneakpeakProps { + fkSchemaName: string; fkTableName: string; fkColumnName: string; value: DatabaseValue; } -function SnippetRow({ fkTableName, fkColumnName, value }: SneakpeakProps) { +function SnippetRow({ + fkSchemaName, + fkTableName, + fkColumnName, + value, +}: SneakpeakProps) { const { databaseDriver } = useDatabaseDriver(); const [data, setData] = useState(); useEffect(() => { databaseDriver - .findFirst("main", fkTableName, { [fkColumnName]: value }) + .findFirst(fkSchemaName, fkTableName, { [fkColumnName]: value }) .then(setData) .catch(console.error); - }, [databaseDriver, fkTableName, fkColumnName, value]); + }, [databaseDriver, fkSchemaName, fkTableName, fkColumnName, value]); if (!data) { return ( @@ -208,6 +214,7 @@ export default function GenericCell({ return (
{ setSchema({ tableName, - schemaName: "main", + schemaName, columns: [], pk: [], autoIncrement: false, diff --git a/src/components/gui/table-combobox/TableCombobox.tsx b/src/components/gui/table-combobox/TableCombobox.tsx index c46905b..552e8ff 100644 --- a/src/components/gui/table-combobox/TableCombobox.tsx +++ b/src/components/gui/table-combobox/TableCombobox.tsx @@ -16,16 +16,18 @@ import { useSchema } from "@/context/schema-provider"; export default function TableCombobox({ value, onChange, + schemaName, disabled, borderless, }: Readonly<{ value?: string; + schemaName: string; onChange: (v: string) => void; disabled?: boolean; borderless?: boolean; }>) { const [open, setOpen] = useState(false); - const { currentSchema, refresh } = useSchema(); + const { schema, refresh } = useSchema(); return ( @@ -48,7 +50,7 @@ export default function TableCombobox({ No table found. - {currentSchema.map((table) => ( + {(schema[schemaName] ?? []).map((table) => (
- +
diff --git a/src/components/lib/sql-execute-helper.ts b/src/components/lib/sql-execute-helper.ts index 5ed1f3b..71a47ce 100644 --- a/src/components/lib/sql-execute-helper.ts +++ b/src/components/lib/sql-execute-helper.ts @@ -86,7 +86,7 @@ export async function commitChange({ try { const result = await driver.updateTableData( - tableSchema.schemaName ?? "main", + tableSchema.schemaName, tableName, plans.map((p) => p.plan), tableSchema diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index e0014f1..0f8a392 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -101,6 +101,7 @@ export type DatabaseForeignKeyAction = | "NO_ACTION"; export interface DatabaseForeignKeyClause { + foreignSchemaName?: string; foreignTableName?: string; foreignColumns?: string[]; columns?: string[]; diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index 999e0da..e826cef 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -52,7 +52,7 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { type: "table", schemaName, name: row.name, - tableSchema: parseCreateTableScript(row.sql), + tableSchema: parseCreateTableScript(schemaName, row.sql), }); } catch { tmp.push({ type: "table", name: row.name, schemaName }); @@ -125,7 +125,7 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { if (def) { const createScript = def.sql; return { - ...parseCreateTableScript(createScript), + ...parseCreateTableScript(schemaName, createScript), createScript, schemaName, }; diff --git a/src/drivers/sqlite/sql-parse-table.test.ts b/src/drivers/sqlite/sql-parse-table.test.ts index e7e954c..c8d7642 100644 --- a/src/drivers/sqlite/sql-parse-table.test.ts +++ b/src/drivers/sqlite/sql-parse-table.test.ts @@ -10,11 +10,11 @@ import { // Parse column constraint function pcc(sql: string) { - return parseColumnConstraint(buildSyntaxCursor(sql)); + return parseColumnConstraint("main", buildSyntaxCursor(sql)); } function p(sql: string) { - return parseCreateTableScript(sql); + return parseCreateTableScript("main", sql); } it("parse column constraint", () => { diff --git a/src/drivers/sqlite/sql-parse-table.ts b/src/drivers/sqlite/sql-parse-table.ts index f9584a7..1f9604a 100644 --- a/src/drivers/sqlite/sql-parse-table.ts +++ b/src/drivers/sqlite/sql-parse-table.ts @@ -122,7 +122,10 @@ export function buildSyntaxCursor(sql: string): Cursor { return new Cursor(r, sql); } -function parseColumnDef(cursor: Cursor): DatabaseTableColumn | null { +function parseColumnDef( + schemaName: string, + cursor: Cursor +): DatabaseTableColumn | null { const columnName = cursor.consumeIdentifier(); if (!columnName) return null; @@ -136,7 +139,7 @@ function parseColumnDef(cursor: Cursor): DatabaseTableColumn | null { cursor.next(); } - const constraint = parseColumnConstraint(cursor); + const constraint = parseColumnConstraint(schemaName, cursor); return { name: columnName, @@ -178,6 +181,7 @@ export function parseColumnList(columnPtr: Cursor) { } export function parseColumnConstraint( + schemaName: string, cursor: Cursor ): DatabaseTableColumnConstraint | undefined { if (cursor.matchKeyword("CONSTRAINT")) { @@ -185,7 +189,7 @@ export function parseColumnConstraint( const constraintName = cursor.consumeIdentifier(); return { - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), name: constraintName, }; } else if (cursor.matchKeyword("PRIMARY")) { @@ -226,7 +230,7 @@ export function parseColumnConstraint( primaryColumns, autoIncrement, primaryKeyConflict: conflict, - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), }; } else if (cursor.matchKeyword("NOT")) { cursor.next(); @@ -237,7 +241,7 @@ export function parseColumnConstraint( return { notNull: true, notNullConflict: conflict, - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), }; } else if (cursor.matchKeyword("UNIQUE")) { let uniqueColumns: string[] | undefined; @@ -256,7 +260,7 @@ export function parseColumnConstraint( unique: true, uniqueConflict: conflict, uniqueColumns, - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), }; } else if (cursor.matchKeyword("DEFAULT")) { let defaultValue: unknown; @@ -296,7 +300,7 @@ export function parseColumnConstraint( return { defaultValue, defaultExpression, - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), }; } else if (cursor.matchKeyword("CHECK")) { cursor.next(); @@ -306,7 +310,7 @@ export function parseColumnConstraint( return { checkExpression: expr, - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), }; } else if (cursor.matchKeyword("COLLATE")) { cursor.next(); @@ -316,7 +320,7 @@ export function parseColumnConstraint( return { collate: collationName, - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), }; } else if (cursor.matchKeyword("FOREIGN")) { cursor.next(); @@ -330,10 +334,11 @@ export function parseColumnConstraint( const columns = parseColumnList(parens); cursor.next(); - const refConstraint = parseColumnConstraint(cursor); + const refConstraint = parseColumnConstraint(schemaName, cursor); return { foreignKey: { + foreignSchemaName: schemaName, foreignTableName: refConstraint?.foreignKey?.foreignTableName ?? "", foreignColumns: refConstraint?.foreignKey?.foreignColumns ?? [], columns, @@ -363,7 +368,7 @@ export function parseColumnConstraint( foreignTableName, foreignColumns, }, - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), }; } else if (cursor.match("GENERATED")) { cursor.next(); @@ -385,14 +390,17 @@ export function parseColumnConstraint( return { generatedType: virtualColumnType, generatedExpression: expr, - ...parseColumnConstraint(cursor), + ...parseColumnConstraint(schemaName, cursor), }; } return undefined; } -function parseTableDefinition(cursor: Cursor): { +function parseTableDefinition( + schemaName: string, + cursor: Cursor +): { columns: DatabaseTableColumn[]; constraints: DatabaseTableColumnConstraint[]; } { @@ -412,13 +420,13 @@ function parseTableDefinition(cursor: Cursor): { "FOREIGN", ]) ) { - const constraint = parseColumnConstraint(cursor); + const constraint = parseColumnConstraint(schemaName, cursor); if (constraint) { constraints.push(constraint); moveNext = true; } } else { - const column = parseColumnDef(cursor); + const column = parseColumnDef(schemaName, cursor); if (column) { columns.push(column); moveNext = true; @@ -496,7 +504,10 @@ function parseFTS5(cursor: Cursor | null): DatabaseTableFts5 { // Our parser follows this spec // https://www.sqlite.org/lang_createtable.html -export function parseCreateTableScript(sql: string): DatabaseTableSchema { +export function parseCreateTableScript( + schemaName: string, + sql: string +): DatabaseTableSchema { const tree = sqliteDialect.language.parser.parse(sql); const ptr = tree.cursor(); @@ -526,7 +537,7 @@ export function parseCreateTableScript(sql: string): DatabaseTableSchema { const defCursor = cursor.enterParens(); const defs = defCursor - ? parseTableDefinition(defCursor) + ? parseTableDefinition(schemaName, defCursor) : { columns: [], constraints: [] }; const pk = defs.columns.filter((col) => col.pk).map((col) => col.name); @@ -537,7 +548,7 @@ export function parseCreateTableScript(sql: string): DatabaseTableSchema { return { tableName, - schemaName: "main", + schemaName, ...defs, pk, autoIncrement, From 15131bcfbec7f23aacbcaa810b037c0c44b2b5e8 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 3 Sep 2024 08:17:19 +0700 Subject: [PATCH 13/17] remove all console.log --- src/components/gui/schema-sidebar-list.tsx | 1 - src/components/lib/sql-execute-helper.ts | 2 -- src/components/listview/index.tsx | 2 -- src/drivers/iframe-driver.ts | 17 ----------------- 4 files changed, 22 deletions(-) diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index b45d5f4..d6813a3 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -172,7 +172,6 @@ export default function SchemaList({ search }: Readonly) { } return r; }, [schema, databaseDriver]); - console.log(listViewItems, schema); const filterCallback = useCallback( (item: ListViewItem) => { diff --git a/src/components/lib/sql-execute-helper.ts b/src/components/lib/sql-execute-helper.ts index 71a47ce..6017590 100644 --- a/src/components/lib/sql-execute-helper.ts +++ b/src/components/lib/sql-execute-helper.ts @@ -92,8 +92,6 @@ export async function commitChange({ tableSchema ); - console.log(result, plans); - data.applyChanges( plans.map((p, idx) => { return { diff --git a/src/components/listview/index.tsx b/src/components/listview/index.tsx index f52384e..1f4ebbd 100644 --- a/src/components/listview/index.tsx +++ b/src/components/listview/index.tsx @@ -271,8 +271,6 @@ export function ListView(props: ListViewProps) { return; } - console.log("here2"); - if (onContextMenu) { const menu = onContextMenu(); if (menu.length === 0) { diff --git a/src/drivers/iframe-driver.ts b/src/drivers/iframe-driver.ts index ea8ee98..8d1bea0 100644 --- a/src/drivers/iframe-driver.ts +++ b/src/drivers/iframe-driver.ts @@ -36,7 +36,6 @@ class IframeConnection { } }; - console.log("register listener"); window.addEventListener("message", handler); return () => window.removeEventListener("message", handler); } @@ -46,12 +45,6 @@ class IframeConnection { const id = ++this.counter; this.queryPromise[id] = { resolve, reject }; - console.log("POST ", { - type: "query", - id, - statement: stmt, - }); - window.parent.postMessage( { type: "query", @@ -68,12 +61,6 @@ class IframeConnection { const id = ++this.counter; this.queryPromise[id] = { resolve, reject }; - console.log("POST ", { - type: "transaction", - id, - statement: stmts, - }); - window.parent.postMessage( { type: "transaction", @@ -97,13 +84,11 @@ export class IframeSQLiteDriver extends SqliteLikeBaseDriver { async query(stmt: string): Promise { const r = await this.conn.query(stmt); - console.log(r); return r; } transaction(stmts: string[]): Promise { const r = this.conn.transaction(stmts); - console.log(r); return r; } } @@ -119,13 +104,11 @@ export class IframeMySQLDriver extends MySQLLikeDriver { async query(stmt: string): Promise { const r = await this.conn.query(stmt); - console.log(r); return r; } transaction(stmts: string[]): Promise { const r = this.conn.transaction(stmts); - console.log(r); return r; } } From 15afa0eaf1615a7dcb8aac065ef9275370426ff3 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 3 Sep 2024 18:49:08 +0700 Subject: [PATCH 14/17] add proper error for iframe driver --- src/components/gui/schema-editor/schema-save-dialog.tsx | 3 +++ src/components/gui/tabs/table-data-tab.tsx | 5 ++++- src/drivers/iframe-driver.ts | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/gui/schema-editor/schema-save-dialog.tsx b/src/components/gui/schema-editor/schema-save-dialog.tsx index f2ec009..5994b77 100644 --- a/src/components/gui/schema-editor/schema-save-dialog.tsx +++ b/src/components/gui/schema-editor/schema-save-dialog.tsx @@ -3,6 +3,7 @@ import { AlertDialogCancel, AlertDialogContent, AlertDialogFooter, + AlertDialogTitle, } from "@/components/ui/alert-dialog"; import CodePreview from "../code-preview"; import { Button } from "@/components/ui/button"; @@ -79,6 +80,8 @@ export default function SchemaSaveDialog({ return ( + Preview + {errorMessage && (
diff --git a/src/components/gui/tabs/table-data-tab.tsx b/src/components/gui/tabs/table-data-tab.tsx index 40973ba..35ca808 100644 --- a/src/components/gui/tabs/table-data-tab.tsx +++ b/src/components/gui/tabs/table-data-tab.tsx @@ -37,6 +37,7 @@ import { useDatabaseDriver } from "@/context/driver-provider"; import ResultStats from "../result-stat"; import isEmptyResultStats from "@/components/lib/empty-stats"; import useTableResultColumnFilter from "../table-result/filter-column"; +import { AlertDialogTitle } from "@radix-ui/react-alert-dialog"; interface TableDataContentProps { tableName: string; @@ -142,6 +143,7 @@ export default function TableDataWindow({ commitChange({ driver: databaseDriver, tableName, tableSchema, data }) .then(({ errorMessage }) => { + console.log("here", errorMessage); if (errorMessage) setExecuteError(errorMessage); }) .catch(console.error) @@ -172,7 +174,8 @@ export default function TableDataWindow({
{executeError && ( - + + Error {executeError} setExecuteError(null)}> diff --git a/src/drivers/iframe-driver.ts b/src/drivers/iframe-driver.ts index 8d1bea0..8738776 100644 --- a/src/drivers/iframe-driver.ts +++ b/src/drivers/iframe-driver.ts @@ -18,7 +18,7 @@ type ParentResponseData = type PromiseResolveReject = { resolve: (value: any) => void; - reject: (value: string) => void; + reject: (value: { message: string }) => void; }; class IframeConnection { @@ -28,7 +28,7 @@ class IframeConnection { listen() { const handler = (e: MessageEvent) => { if (e.data.error) { - this.queryPromise[e.data.id].reject(e.data.error); + this.queryPromise[e.data.id].reject({ message: e.data.error }); delete this.queryPromise[e.data.id]; } else { this.queryPromise[e.data.id].resolve(e.data.data); From 76a3f23018bd1836f394364c96683712d460607a Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 3 Sep 2024 19:31:12 +0700 Subject: [PATCH 15/17] add better logging and mysql ignore system database --- src/components/gui/studio.tsx | 30 ++++++++++++++++++++-- src/components/gui/tabs/table-data-tab.tsx | 1 - src/drivers/mysql/mysql-driver.ts | 7 ++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/components/gui/studio.tsx b/src/components/gui/studio.tsx index 800dd56..7cb6d83 100644 --- a/src/components/gui/studio.tsx +++ b/src/components/gui/studio.tsx @@ -3,7 +3,7 @@ import MainScreen from "@/components/gui/main-connection"; import { ConfigProvider } from "@/context/config-provider"; import { DriverProvider } from "@/context/driver-provider"; import type { BaseDriver } from "@/drivers/base-driver"; -import { ReactElement } from "react"; +import { ReactElement, useMemo } from "react"; import OptimizeTableState from "@/components/gui/table-optimized/OptimizeTableState"; import { StudioContextMenuItem } from "@/messages/open-context-menu"; import { CollaborationBaseDriver } from "@/drivers/collaboration-driver-base"; @@ -36,9 +36,35 @@ export function Studio({ sideBarFooterComponent, onBack, }: Readonly) { + const proxyDriver = useMemo(() => { + return new Proxy(driver, { + get(...arg) { + const [target, property] = arg; + + if (property === "query") { + return async (statement: string) => { + console.group("Query"); + console.info(`%c${statement}`, "color:#e67e22"); + console.groupEnd(); + return await target.query(statement); + }; + } else if (property === "transaction") { + return async (statements: string[]) => { + console.group("Transaction"); + statements.forEach((s) => console.log(`%c${s}`, "color:#e67e22")); + console.groupEnd(); + return await target.transaction(statements); + }; + } + + return Reflect.get(...arg); + }, + }); + }, [driver]); + return ( diff --git a/src/components/gui/tabs/table-data-tab.tsx b/src/components/gui/tabs/table-data-tab.tsx index 35ca808..1569f8c 100644 --- a/src/components/gui/tabs/table-data-tab.tsx +++ b/src/components/gui/tabs/table-data-tab.tsx @@ -143,7 +143,6 @@ export default function TableDataWindow({ commitChange({ driver: databaseDriver, tableName, tableSchema, data }) .then(({ errorMessage }) => { - console.log("here", errorMessage); if (errorMessage) setExecuteError(errorMessage); }) .catch(console.error) diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index 712d070..e094f8a 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -55,17 +55,18 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { } async schemas(): Promise { - const schemaSql = "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA"; + const schemaSql = + "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')"; const schemaResult = (await this.query(schemaSql)) .rows as unknown as MySqlDatabase[]; const tableSql = - "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables"; + "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')"; const tableResult = (await this.query(tableSql)) .rows as unknown as MySqlTable[]; const columnSql = - "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, EXTRA FROM information_schema.columns"; + "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, EXTRA FROM information_schema.columns WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')"; const columnResult = (await this.query(columnSql)) .rows as unknown as MySqlColumn[]; From e04ae49b57cbead241a8fe0402738e7ee7e4620d Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Thu, 5 Sep 2024 20:15:54 +0700 Subject: [PATCH 16/17] fixing test case --- src/drivers/sqlite/sql-parse-table.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/drivers/sqlite/sql-parse-table.test.ts b/src/drivers/sqlite/sql-parse-table.test.ts index c8d7642..77d3700 100644 --- a/src/drivers/sqlite/sql-parse-table.test.ts +++ b/src/drivers/sqlite/sql-parse-table.test.ts @@ -79,6 +79,7 @@ it("parse column constraint", () => { pcc(`foreign key ("user_id") references "users" on delete cascade ("id")`) ).toEqual({ foreignKey: { + foreignSchemaName: "main", columns: ["user_id"], foreignTableName: "users", foreignColumns: ["id"], @@ -183,6 +184,7 @@ it("parse create table with table constraints", () => { }, { foreignKey: { + foreignSchemaName: "main", columns: ["category_id"], foreignColumns: ["id"], foreignTableName: "category", From f949990b98b952ceab7920316ee73432dacde827 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Thu, 5 Sep 2024 20:32:46 +0700 Subject: [PATCH 17/17] fixing the type --- src/components/gui/schema-editor/column-provider.tsx | 2 +- src/components/gui/table-cell/generic-cell.tsx | 3 ++- src/drivers/mysql/mysql-driver.ts | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/gui/schema-editor/column-provider.tsx b/src/components/gui/schema-editor/column-provider.tsx index f47a05a..451b054 100644 --- a/src/components/gui/schema-editor/column-provider.tsx +++ b/src/components/gui/schema-editor/column-provider.tsx @@ -1,5 +1,5 @@ +import { DatabaseTableColumnChange } from "@/drivers/base-driver"; import { PropsWithChildren, createContext, useContext } from "react"; -import { DatabaseTableColumnChange } from "."; const ColumnContext = createContext<{ columns: DatabaseTableColumnChange[] }>({ columns: [], diff --git a/src/components/gui/table-cell/generic-cell.tsx b/src/components/gui/table-cell/generic-cell.tsx index e65737a..21be342 100644 --- a/src/components/gui/table-cell/generic-cell.tsx +++ b/src/components/gui/table-cell/generic-cell.tsx @@ -209,7 +209,8 @@ export default function GenericCell({ const fkContent = useMemo(() => { if ( header.foreignKey?.foreignTableName && - header.foreignKey.foreignColumns + header.foreignKey.foreignColumns && + header.foreignKey?.foreignSchemaName ) { return (
diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index e094f8a..011c7c2 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -107,8 +107,9 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { }; const tableKey = c.TABLE_SCHEMA + "." + c.TABLE_NAME; - if (tableRecord[tableKey].tableSchema) { - tableRecord[tableKey].tableSchema.columns.push(column); + const tableSchema = tableRecord[tableKey].tableSchema; + if (tableSchema) { + tableSchema.columns.push(column); } }