diff --git a/packages/graphql-server/schema.gql b/packages/graphql-server/schema.gql index 17dc2772..4f201e0d 100644 --- a/packages/graphql-server/schema.gql +++ b/packages/graphql-server/schema.gql @@ -10,6 +10,7 @@ type Column { name: String! isPrimaryKey: Boolean! type: String! + sourceTable: String constraints: [ColConstraint!] } @@ -226,7 +227,9 @@ type Query { docOrDefaultDoc(refName: String!, databaseName: String!, docType: DocType): Doc rowDiffs(offset: Int, databaseName: String!, fromCommitId: String!, toCommitId: String!, refName: String, tableName: String!, filterByRowType: DiffRowType): RowDiffList! rows(refName: String!, databaseName: String!, tableName: String!, offset: Int): RowList! + doltSchemas(databaseName: String!, refName: String!): RowList! views(databaseName: String!, refName: String!): RowList! + doltProcedures(databaseName: String!, refName: String!): RowList schemaDiff(databaseName: String!, fromCommitId: String!, toCommitId: String!, refName: String, tableName: String!): SchemaDiff sqlSelect(refName: String!, databaseName: String!, queryString: String!): SqlSelect! sqlSelectForCsvDownload(refName: String!, databaseName: String!, queryString: String!): String! diff --git a/packages/graphql-server/src/columns/column.model.ts b/packages/graphql-server/src/columns/column.model.ts index f3abade7..e8c1d8fa 100644 --- a/packages/graphql-server/src/columns/column.model.ts +++ b/packages/graphql-server/src/columns/column.model.ts @@ -18,15 +18,19 @@ export class Column { @Field() type: string; + @Field({ nullable: true }) + sourceTable?: string; + @Field(_type => [ColConstraint], { nullable: true }) constraints?: ColConstraint[]; } -export function fromDoltRowRes(col: RawRow): Column { +export function fromDoltRowRes(col: RawRow, tableName: string): Column { return { name: col.Field, isPrimaryKey: col.Key === "PRI", type: col.Type, constraints: [{ notNull: col.Null === "NO" }], + sourceTable: tableName, }; } diff --git a/packages/graphql-server/src/rowDiffs/rowDiff.model.ts b/packages/graphql-server/src/rowDiffs/rowDiff.model.ts index c22f62f0..6b52b372 100644 --- a/packages/graphql-server/src/rowDiffs/rowDiff.model.ts +++ b/packages/graphql-server/src/rowDiffs/rowDiff.model.ts @@ -58,11 +58,12 @@ export function fromDoltListRowWithColsRes( rows: RawRow[], cols: RawRow[], offset: number, + tableName: string, ): RowListWithCols { return { list: rows.slice(0, ROW_LIMIT).map(row.fromDoltRowRes), nextOffset: getNextOffset(rows.length, offset), - columns: cols.map(columns.fromDoltRowRes), + columns: cols.map(c => columns.fromDoltRowRes(c, tableName)), }; } diff --git a/packages/graphql-server/src/rowDiffs/rowDiff.resolver.ts b/packages/graphql-server/src/rowDiffs/rowDiff.resolver.ts index d41ff59d..6695293e 100644 --- a/packages/graphql-server/src/rowDiffs/rowDiff.resolver.ts +++ b/packages/graphql-server/src/rowDiffs/rowDiff.resolver.ts @@ -93,8 +93,8 @@ export class RowDiffResolver { ]); const colsUnion = unionCols( - oldCols.map(column.fromDoltRowRes), - newCols.map(column.fromDoltRowRes), + oldCols.map(c => column.fromDoltRowRes(c, fromTableName)), + newCols.map(c => column.fromDoltRowRes(c, toTableName)), ); const diffType = convertToStringForQuery(args.filterByRowType); @@ -129,5 +129,5 @@ async function getRowsForDiff(query: ParQuery, args: ListRowsArgs) { ROW_LIMIT + 1, offset, ]); - return fromDoltListRowWithColsRes(rows, columns, offset); + return fromDoltListRowWithColsRes(rows, columns, offset, args.tableName); } diff --git a/packages/graphql-server/src/rows/row.queries.ts b/packages/graphql-server/src/rows/row.queries.ts index 4cda1897..7bc905f5 100644 --- a/packages/graphql-server/src/rows/row.queries.ts +++ b/packages/graphql-server/src/rows/row.queries.ts @@ -2,10 +2,13 @@ import { RawRows } from "../utils/commonTypes"; export const getRowsQuery = ( columns: RawRows, + hasWhereCause = false, ): { q: string; cols: string[] } => { const cols = getPKColsForRowsQuery(columns); return { - q: `SELECT * FROM ?? ${getOrderByFromCols(cols.length)}LIMIT ? OFFSET ?`, + q: `SELECT * FROM ?? ${ + hasWhereCause ? "WHERE type = ? " : "" + }${getOrderByFromCols(cols.length)}LIMIT ? OFFSET ?`, cols, }; }; diff --git a/packages/graphql-server/src/rows/row.resolver.ts b/packages/graphql-server/src/rows/row.resolver.ts index c1c91bb8..1de551f2 100644 --- a/packages/graphql-server/src/rows/row.resolver.ts +++ b/packages/graphql-server/src/rows/row.resolver.ts @@ -1,4 +1,5 @@ import { Args, ArgsType, Field, Int, Query, Resolver } from "@nestjs/graphql"; +import { handleTableNotFound } from "src/tables/table.resolver"; import { DataSourceService } from "../dataSources/dataSource.service"; import { DoltSystemTable } from "../systemTables/systemTable.enums"; import { listTablesQuery } from "../tables/table.queries"; @@ -40,11 +41,42 @@ export class RowResolver { ); } + @Query(_returns => RowList) + async doltSchemas( + @Args() args: RefArgs, + type?: string, + ): Promise { + return this.dss.queryMaybeDolt( + async query => { + const tableName = DoltSystemTable.SCHEMAS; + const columns = await query(listTablesQuery, [tableName]); + + const page = { columns, offset: 0 }; + const { q, cols } = getRowsQuery(columns, !!type); + + const params = [...cols, ROW_LIMIT + 1, page.offset]; + const rows = await query( + q, + type ? [tableName, type, ...params] : [tableName, ...params], + ); + return fromDoltListRowRes(rows, page.offset); + }, + args.databaseName, + args.refName, + ); + } + @Query(_returns => RowList) async views(@Args() args: RefArgs): Promise { - return this.rows({ + return this.doltSchemas(args, "view"); + } + + @Query(_returns => RowList, { nullable: true }) + async doltProcedures(@Args() args: RefArgs): Promise { + const tableArgs = { ...args, - tableName: DoltSystemTable.SCHEMAS, - }); + tableName: DoltSystemTable.PROCEDURES, + }; + return handleTableNotFound(async () => this.rows(tableArgs)); } } diff --git a/packages/graphql-server/src/systemTables/systemTable.enums.ts b/packages/graphql-server/src/systemTables/systemTable.enums.ts index cdb0070b..b5d09e54 100644 --- a/packages/graphql-server/src/systemTables/systemTable.enums.ts +++ b/packages/graphql-server/src/systemTables/systemTable.enums.ts @@ -2,6 +2,7 @@ export enum DoltSystemTable { DOCS = "dolt_docs", QUERY_CATALOG = "dolt_query_catalog", SCHEMAS = "dolt_schemas", + PROCEDURES = "dolt_procedures", } export const systemTableValues = Object.values(DoltSystemTable); diff --git a/packages/graphql-server/src/tables/table.model.ts b/packages/graphql-server/src/tables/table.model.ts index 262b9e22..8a3ac796 100644 --- a/packages/graphql-server/src/tables/table.model.ts +++ b/packages/graphql-server/src/tables/table.model.ts @@ -47,7 +47,7 @@ export function fromDoltRowRes( databaseName, refName, tableName, - columns: columns.map(column.fromDoltRowRes), + columns: columns.map(c => column.fromDoltRowRes(c, tableName)), foreignKeys: foreignKey.fromDoltRowsRes(fkRows), indexes: index.fromDoltRowsRes(idxRows), }; diff --git a/packages/web/components/CellButtons/DropColumnButton.tsx b/packages/web/components/CellButtons/DropColumnButton.tsx index 46c89e9e..d78b7cee 100644 --- a/packages/web/components/CellButtons/DropColumnButton.tsx +++ b/packages/web/components/CellButtons/DropColumnButton.tsx @@ -17,10 +17,12 @@ export default function DropColumnButton({ col, refName }: Props) { const { params } = useDataTableContext(); const { tableName } = params; - if (!tableName || isDoltSystemTable(tableName)) return null; + if (!tableName || !col.sourceTable || isDoltSystemTable(tableName)) { + return null; + } const onClick = async () => { - const query = dropColumnQuery(tableName, col.name); + const query = dropColumnQuery(col.sourceTable ?? tableName, col.name); setEditorString(query); await executeQuery({ ...params, diff --git a/packages/web/components/CellButtons/HideColumnButton.tsx b/packages/web/components/CellButtons/HideColumnButton.tsx index 41433b55..eb491dc1 100644 --- a/packages/web/components/CellButtons/HideColumnButton.tsx +++ b/packages/web/components/CellButtons/HideColumnButton.tsx @@ -14,10 +14,9 @@ export default function HideColumnButton({ col, columns }: Props) { const { executeQuery } = useSqlEditorContext(); const { params } = useDataTableContext(); const q = params.q ?? `SELECT * FROM \`${params.tableName}\``; - const colNames = columns.map(c => c.name); const onClick = async () => { - const query = removeColumnFromQuery(q, col.name, colNames); + const query = removeColumnFromQuery(q, col.name, columns); await executeQuery({ ...params, query }); }; diff --git a/packages/web/components/DataTable/ShowAllColumns.tsx b/packages/web/components/DataTable/ShowAllColumns.tsx index bc4e186a..fa91192e 100644 --- a/packages/web/components/DataTable/ShowAllColumns.tsx +++ b/packages/web/components/DataTable/ShowAllColumns.tsx @@ -6,21 +6,19 @@ import css from "./index.module.css"; export default function ShowAllColumns() { const { executeQuery } = useSqlEditorContext(); - const { params } = useDataTableContext(); - const { tableName } = params; + const { params, tableNames } = useDataTableContext(); - if (!tableName) return null; + if (!params.tableName || tableNames.length > 1) return null; const q = params.q ?? `SELECT * FROM \`${params.tableName}\``; - const cols = getColumns(q); + const col = getColumns(q); - if (!cols?.length || cols[0].expr.column === "*") return null; + if (!col?.length || col[0].expr.column === "*") return null; const onClick = async () => { - const query = convertToSqlWithNewCols(q, "*", tableName); + const query = convertToSqlWithNewCols(q, "*", tableNames); await executeQuery({ ...params, query }); }; - return ( Show all columns diff --git a/packages/web/components/DataTable/Table/Cell.tsx b/packages/web/components/DataTable/Table/Cell.tsx index 3f5ea334..e0cb499f 100644 --- a/packages/web/components/DataTable/Table/Cell.tsx +++ b/packages/web/components/DataTable/Table/Cell.tsx @@ -85,7 +85,7 @@ export default function Cell(props: Props): JSX.Element { }} /> ) : ( - {displayCellVal} + setEditing(true)}>{displayCellVal} )} {!editing && ( @@ -56,21 +56,21 @@ export default function NavLinks({ className, params }: Props) { {params.refName ? ( - + + + ) : ( -

- No schemas to show +

+ No views to show

)}
{params.refName ? ( - - - + ) : ( -

- No views to show +

+ No schemas to show

)}
@@ -93,9 +93,9 @@ function getActiveIndexFromRouterQuery( switch (activeQuery) { case "Tables": return 0; - case "Schemas": - return 1; case "Views": + return 1; + case "Schemas": return 2; default: return 0; diff --git a/packages/web/components/DatabaseTableNav/index.module.css b/packages/web/components/DatabaseTableNav/index.module.css index 3420ee10..503038e3 100644 --- a/packages/web/components/DatabaseTableNav/index.module.css +++ b/packages/web/components/DatabaseTableNav/index.module.css @@ -6,7 +6,7 @@ } .openContainer { - @apply transition-width w-96 min-w-[22rem] duration-75; + @apply transition-width w-96 min-w-[24rem] duration-75; flex-basis: 24rem; } diff --git a/packages/web/components/HistoryTable/queryHelpers.ts b/packages/web/components/HistoryTable/queryHelpers.ts index ac3c8518..be615ea8 100644 --- a/packages/web/components/HistoryTable/queryHelpers.ts +++ b/packages/web/components/HistoryTable/queryHelpers.ts @@ -1,6 +1,6 @@ import { removeClauses } from "@lib/doltSystemTables"; import { - convertToSqlWithNewCols, + convertToSqlWithNewColNames, getColumns, getTableName, } from "@lib/parseSqlQuery"; @@ -26,7 +26,7 @@ export function getDoltHistoryQuery(q: string): string { // SELECT [cols] FROM dolt_history_[tableName] WHERE [conditions]; const query = formatQuery(q); - return convertToSqlWithNewCols(query, cols, historyTableName); + return convertToSqlWithNewColNames(query, cols, historyTableName); } function formatQuery(q: string): string { diff --git a/packages/web/components/ViewFragment/index.module.css b/packages/web/components/SchemaFragment/index.module.css similarity index 100% rename from packages/web/components/ViewFragment/index.module.css rename to packages/web/components/SchemaFragment/index.module.css diff --git a/packages/web/components/ViewFragment/index.tsx b/packages/web/components/SchemaFragment/index.tsx similarity index 84% rename from packages/web/components/ViewFragment/index.tsx rename to packages/web/components/SchemaFragment/index.tsx index 35835ea5..051b96f5 100644 --- a/packages/web/components/ViewFragment/index.tsx +++ b/packages/web/components/SchemaFragment/index.tsx @@ -12,6 +12,7 @@ import { SqlQueryParams } from "@lib/params"; import { MdPlayCircleOutline } from "@react-icons/all-files/md/MdPlayCircleOutline"; import dynamic from "next/dynamic"; import css from "./index.module.css"; +import { getSchemaInfo } from "./util"; const AceEditor = dynamic(async () => import("@components/AceEditor"), { ssr: false, @@ -26,6 +27,7 @@ type InnerProps = Props & { }; function Inner({ rows, params }: InnerProps) { + const { isView, fragIdx } = getSchemaInfo(params.q); const { queryClickHandler } = useSqlEditorContext("Views"); const { isMobile } = useReactiveWidth(null, 1024); @@ -40,7 +42,7 @@ function Inner({ rows, params }: InnerProps) { } const name = rows[0].columnValues[0].displayValue; - const fragment = rows[0].columnValues[1].displayValue; + const fragment = rows[0].columnValues[fragIdx].displayValue; return (
@@ -56,16 +58,18 @@ function Inner({ rows, params }: InnerProps) { height={isMobile ? "calc(100vh - 38rem)" : "calc(100vh - 28rem)"} />
- executeView(name)}> - - + {isView && ( + executeView(name)}> + + + )}
); } -export default function ViewFragment(props: Props) { +export default function SchemaFragment(props: Props) { const res = useSqlSelectForSqlDataTableQuery({ variables: { ...props.params, queryString: props.params.q }, }); diff --git a/packages/web/components/SchemaFragment/util.ts b/packages/web/components/SchemaFragment/util.ts new file mode 100644 index 00000000..f9acaf3e --- /dev/null +++ b/packages/web/components/SchemaFragment/util.ts @@ -0,0 +1,16 @@ +export function getSchemaInfo(q: string): { isView: boolean; fragIdx: number } { + const lower = q.toLowerCase(); + if (lower.startsWith("show create view")) { + return { isView: true, fragIdx: 1 }; + } + if ( + lower.startsWith("show create trigger") || + lower.startsWith("show create procedure") + ) { + return { isView: false, fragIdx: 2 }; + } + if (lower.startsWith("show create event")) { + return { isView: false, fragIdx: 3 }; + } + return { isView: false, fragIdx: 0 }; +} diff --git a/packages/web/components/SchemaList/Item.tsx b/packages/web/components/SchemaList/Item.tsx index 58689445..7deebfce 100644 --- a/packages/web/components/SchemaList/Item.tsx +++ b/packages/web/components/SchemaList/Item.tsx @@ -2,47 +2,37 @@ import Btn from "@components/Btn"; import Link from "@components/links/Link"; import { RefParams } from "@lib/params"; import { sqlQuery } from "@lib/urls"; -import { MdPlayCircleOutline } from "@react-icons/all-files/md/MdPlayCircleOutline"; +import { RiBookOpenLine } from "@react-icons/all-files/ri/RiBookOpenLine"; import cx from "classnames"; import css from "./index.module.css"; -import { tableIsActive } from "./utils"; type Props = { - tableName: string; - params: RefParams & { q?: string }; + name: string; + params: RefParams; + isActive: boolean; + query: string; }; -export default function Item({ tableName, params }: Props) { - const active = tableIsActive(tableName, params.q); +export default function Item({ name, params, isActive, query }: Props) { return (
  • -
    - {tableName} - - {active ? ( - Viewing - ) : ( - - - - - - )} - -
    + + + {name} + + {isActive ? "Viewing" : } + + +
  • ); } diff --git a/packages/web/components/SchemaList/List.tsx b/packages/web/components/SchemaList/List.tsx new file mode 100644 index 00000000..347416b6 --- /dev/null +++ b/packages/web/components/SchemaList/List.tsx @@ -0,0 +1,56 @@ +import SmallLoader from "@components/SmallLoader"; +import { RefParams } from "@lib/params"; +import { pluralize } from "@lib/pluralize"; +import { useEffect } from "react"; +import Item from "./Item"; +import NotFound from "./NotFound"; +import css from "./index.module.css"; +import { SchemaKind, getActiveItem } from "./utils"; + +type InnerProps = { + params: RefParams & { q?: string }; + items: string[]; + kind: SchemaKind; + loading?: boolean; +}; + +export default function List(props: InnerProps) { + const activeItem = getActiveItem(props.kind, props.params.q); + const pluralKind = pluralize(2, props.kind); + + useEffect(() => { + if (!activeItem) return; + const el = document.getElementById(activeItem); + el?.scrollIntoView(); + }); + + if (props.loading) { + return ( + + ); + } + + return ( +
    + {props.items.length ? ( +
      + {props.items.map(t => ( + + ))} +
    + ) : ( + + )} +
    + ); +} diff --git a/packages/web/components/SchemaList/NotFound.tsx b/packages/web/components/SchemaList/NotFound.tsx new file mode 100644 index 00000000..9178e681 --- /dev/null +++ b/packages/web/components/SchemaList/NotFound.tsx @@ -0,0 +1,15 @@ +import { RefParams } from "@lib/params"; +import css from "./index.module.css"; + +type Props = { + name: string; + params: RefParams; +}; + +export default function NotFound(props: Props) { + return ( +

    + No {props.name} found on {props.params.refName} +

    + ); +} diff --git a/packages/web/components/SchemaList/Procedures.tsx b/packages/web/components/SchemaList/Procedures.tsx new file mode 100644 index 00000000..e88d4262 --- /dev/null +++ b/packages/web/components/SchemaList/Procedures.tsx @@ -0,0 +1,43 @@ +import SmallLoader from "@components/SmallLoader"; +import QueryHandler from "@components/util/QueryHandler"; +import { useRowsForDoltProceduresQuery } from "@gen/graphql-types"; +import { RefParams } from "@lib/params"; +import List from "./List"; +import css from "./index.module.css"; + +type Props = { + params: RefParams & { q?: string }; +}; + +export default function Procedures(props: Props) { + const res = useRowsForDoltProceduresQuery({ + variables: { + databaseName: props.params.databaseName, + refName: props.params.refName, + }, + }); + + return ( + + } + render={data => ( + e.columnValues[0].displayValue, + ) ?? [] + } + kind="procedure" + /> + )} + /> + ); +} diff --git a/packages/web/components/SchemaList/Tables.tsx b/packages/web/components/SchemaList/Tables.tsx new file mode 100644 index 00000000..bfc0dff2 --- /dev/null +++ b/packages/web/components/SchemaList/Tables.tsx @@ -0,0 +1,31 @@ +import SmallLoader from "@components/SmallLoader"; +import QueryHandler from "@components/util/QueryHandler"; +import useTableNames from "@hooks/useTableNames"; +import { RefParams } from "@lib/params"; +import List from "./List"; +import css from "./index.module.css"; + +type Props = { + params: RefParams & { q?: string }; +}; + +export default function Tables(props: Props) { + const res = useTableNames({ + databaseName: props.params.databaseName, + refName: props.params.refName, + }); + + return ( + + } + render={data => } + /> + ); +} diff --git a/packages/web/components/SchemaList/index.module.css b/packages/web/components/SchemaList/index.module.css index cd53b5c2..4318cb77 100644 --- a/packages/web/components/SchemaList/index.module.css +++ b/packages/web/components/SchemaList/index.module.css @@ -1,55 +1,56 @@ -.item { - @apply text-primary px-3; - @screen lg { - @apply p-0; +.container { + @apply mb-10; + + h4 { + @apply mx-3 mb-1.5 mt-3 text-base; } } -.buttonIcon { - @apply text-ld-darkgrey text-2xl mr-4 h-6 rounded-full; +.text { + @apply mb-5 mt-2 mx-5; +} - &:focus { - @apply widget-shadow-lightblue outline-none; - } +.icon { + @apply text-xl mr-2 text-primary rounded-full opacity-25; } -.active, -.item:hover { - @apply bg-white; +.item { + @apply w-full py-2.5 px-6; - .buttonIcon { - @apply font-semibold; + @screen lg { + @apply pr-2; } } -.table { - @apply flex justify-between text-sm; -} - -.tableName { - @apply p-3 text-acc-hoverlinkblue font-semibold w-full; +.item:hover .icon { + @apply opacity-100; &:hover { - @apply text-acc-linkblue; + @apply text-acc-hoverblue; } } -.right { - @apply flex items-center; +.button { + @apply flex justify-between items-center w-full; + + &:focus { + @apply outline-none widget-shadow-lightblue; + } } -.tableStatus { - @apply text-ld-orange pr-4 mr-1 font-semibold; +.selected, +.item:hover { + @apply bg-white text-acc-linkblue; } -.item:hover .buttonIcon { - @apply text-primary; +.name { + @apply text-acc-hoverlinkblue text-sm font-semibold; +} - &:hover { - @apply text-acc-hoverblue; - } +.viewing { + @apply py-0.5 mr-4 text-ld-orange font-semibold text-sm; } -.empty { - @apply my-4 mx-5; +.smallLoader { + @apply text-sm; } diff --git a/packages/web/components/SchemaList/index.test.tsx b/packages/web/components/SchemaList/index.test.tsx index f873d3e5..6581ceb5 100644 --- a/packages/web/components/SchemaList/index.test.tsx +++ b/packages/web/components/SchemaList/index.test.tsx @@ -1,12 +1,11 @@ import { MockedProvider } from "@apollo/client/testing"; -import { RefParams } from "@lib/params"; import { render, screen } from "@testing-library/react"; import SchemaList from "."; import * as mocks from "./mocks"; -const params: RefParams = { - databaseName: "dbname", +const params = { refName: "main", + databaseName: "dbname", }; const tableList = [[], [mocks.tableOne], [mocks.tableOne, mocks.tableTwo]]; @@ -26,7 +25,7 @@ describe("test SchemaList", () => { ); if (tables.length === 0) { - expect(await screen.findByText("No tables found for")).toBeVisible(); + expect(await screen.findByText("No tables found on")).toBeVisible(); } else { tables.forEach(async table => { expect(await screen.findByText(table)).toBeVisible(); diff --git a/packages/web/components/SchemaList/index.tsx b/packages/web/components/SchemaList/index.tsx index 470c4026..7250df1b 100644 --- a/packages/web/components/SchemaList/index.tsx +++ b/packages/web/components/SchemaList/index.tsx @@ -1,61 +1,68 @@ import Section from "@components/DatabaseTableNav/Section"; -import SchemaDiagramButton from "@components/SchemaDiagramButton"; -import QueryHandler from "@components/util/QueryHandler"; -import useTableNames from "@hooks/useTableNames"; +import NotDoltWrapper from "@components/util/NotDoltWrapper"; +import { useRowsForDoltSchemasQuery } from "@gen/graphql-types"; import { RefParams } from "@lib/params"; -import { useEffect } from "react"; -import Item from "./Item"; +import List from "./List"; +import Procedures from "./Procedures"; +import Tables from "./Tables"; import css from "./index.module.css"; -import { getActiveTable } from "./utils"; +import { getSchemaItemsFromRows } from "./utils"; type Props = { params: RefParams & { q?: string }; }; -type InnerProps = Props & { - tables: string[]; -}; - -function Inner(props: InnerProps) { - const activeTable = getActiveTable(props.params.q); - - useEffect(() => { - if (!activeTable) return; - const el = document.getElementById(activeTable); - el?.scrollIntoView(); - }); +function DoltFeatures(props: Props) { + const res = useRowsForDoltSchemasQuery({ variables: props.params }); + return ( + <> +

    Views

    + +

    Triggers

    + +

    Events

    + +

    Procedures

    + + + ); +} +function Inner(props: Props) { return ( -
    - {props.tables.length ? ( - <> -
      - {props.tables.map(t => ( - - ))} -
    - - - ) : ( -

    - No tables found for {props.params.refName} -

    - )} +
    +

    Tables

    + + {/* TODO: Make this work for non-Dolt */} + + +
    ); } export default function SchemaList(props: Props) { - const res = useTableNames({ - databaseName: props.params.databaseName, - refName: props.params.refName, - }); return ( -
    - } - /> +
    +
    ); } diff --git a/packages/web/components/SchemaList/queries.ts b/packages/web/components/SchemaList/queries.ts new file mode 100644 index 00000000..d99973d9 --- /dev/null +++ b/packages/web/components/SchemaList/queries.ts @@ -0,0 +1,21 @@ +import { gql } from "@apollo/client"; + +export const ROWS_FOR_SCHEMAS = gql` + query RowsForDoltSchemas($databaseName: String!, $refName: String!) { + doltSchemas(databaseName: $databaseName, refName: $refName) { + list { + ...RowForSchemas + } + } + } +`; + +export const ROWS_FOR_PROCEDURES = gql` + query RowsForDoltProcedures($databaseName: String!, $refName: String!) { + doltProcedures(databaseName: $databaseName, refName: $refName) { + list { + ...RowForSchemas + } + } + } +`; diff --git a/packages/web/components/SchemaList/utils.ts b/packages/web/components/SchemaList/utils.ts index de22d647..15782c2a 100644 --- a/packages/web/components/SchemaList/utils.ts +++ b/packages/web/components/SchemaList/utils.ts @@ -1,13 +1,22 @@ -export function tableIsActive(tableName: string, q?: string): boolean { - if (!q) return false; - const qf = q.toLowerCase().trim(); - const text = "show create table"; - const tn = tableName.toLowerCase(); - return qf === `${text} ${tn}` || qf === `${text} \`${tn}\``; -} +import { RowForSchemasFragment } from "@gen/graphql-types"; + +export type SchemaKind = "table" | "view" | "trigger" | "event" | "procedure"; -export function getActiveTable(q?: string): string | undefined { +export function getActiveItem( + kind: SchemaKind, + q?: string, +): string | undefined { if (!q) return undefined; - const table = q.replace(/show create table /gi, ""); - return table.replaceAll("`", ""); + const item = q.toLowerCase().replace(`show create ${kind} `, ""); + return item.replaceAll("`", ""); +} + +export function getSchemaItemsFromRows( + kind: SchemaKind, + rows?: RowForSchemasFragment[], +): string[] { + if (!rows) return []; + return rows + .filter(v => v.columnValues[0].displayValue === kind) + .map(e => e.columnValues[1].displayValue); } diff --git a/packages/web/components/SqlDataTable/queries.ts b/packages/web/components/SqlDataTable/queries.ts index 8d084194..ae42547a 100644 --- a/packages/web/components/SqlDataTable/queries.ts +++ b/packages/web/components/SqlDataTable/queries.ts @@ -10,6 +10,7 @@ export const SQL_SELECT_QUERY = gql` name isPrimaryKey type + sourceTable } query SqlSelectForSqlDataTable( $databaseName: String! diff --git a/packages/web/components/TableList/ColumnList/index.tsx b/packages/web/components/TableList/ColumnList/index.tsx index 697d4c2b..dc10c4aa 100644 --- a/packages/web/components/TableList/ColumnList/index.tsx +++ b/packages/web/components/TableList/ColumnList/index.tsx @@ -54,7 +54,7 @@ function ColumnItem({ col }: { col: ColumnForTableListFragment }) { data-cy={`db-tables-table-column-${col.name}`} >
    - {excerpt(col.name, 24)} + {excerpt(col.name, 30)} {col.isPrimaryKey && ( diff --git a/packages/web/components/TableList/Item/index.module.css b/packages/web/components/TableList/Item/index.module.css index f37a1921..fa02c598 100644 --- a/packages/web/components/TableList/Item/index.module.css +++ b/packages/web/components/TableList/Item/index.module.css @@ -3,7 +3,7 @@ } .buttonIcon { - @apply text-ld-darkgrey text-2xl mr-4 h-6 rounded-full; + @apply text-primary opacity-25 text-2xl mr-4 h-6 rounded-full; &:focus { @apply widget-shadow-lightblue outline-none; @@ -20,7 +20,7 @@ } .item:hover .buttonIcon { - @apply text-primary; + @apply opacity-100; &:hover { @apply text-acc-hoverblue; @@ -64,7 +64,7 @@ } .right { - @apply flex items-center; + @apply flex items-center mt-1; a { @apply hidden lg:block; diff --git a/packages/web/components/Views/ViewItem.tsx b/packages/web/components/Views/ViewItem.tsx index e463ccd1..bb32f9da 100644 --- a/packages/web/components/Views/ViewItem.tsx +++ b/packages/web/components/Views/ViewItem.tsx @@ -1,21 +1,20 @@ import Btn from "@components/Btn"; import { useSqlEditorContext } from "@contexts/sqleditor"; -import { RowForViewsFragment } from "@gen/graphql-types"; +import { RowForSchemasFragment } from "@gen/graphql-types"; import { RefParams } from "@lib/params"; import { MdPlayCircleOutline } from "@react-icons/all-files/md/MdPlayCircleOutline"; -import { RiBookOpenLine } from "@react-icons/all-files/ri/RiBookOpenLine"; import cx from "classnames"; import css from "./index.module.css"; type Props = { params: RefParams & { q?: string }; - view: RowForViewsFragment; + view: RowForSchemasFragment; }; export default function ViewItem(props: Props) { const name = props.view.columnValues[1].displayValue; const { queryClickHandler } = useSqlEditorContext("Views"); - const { viewingQuery, viewingDef } = isActive(name, props.params.q); + const viewingQuery = isActive(name, props.params.q); const id = `view-${name}`; const executeView = async () => { @@ -23,53 +22,30 @@ export default function ViewItem(props: Props) { await queryClickHandler({ ...props.params, query }); }; - const executeShowView = async () => { - const query = `SHOW CREATE VIEW \`${name}\``; - await queryClickHandler({ ...props.params, query }); - }; - return (
  • -
    {name}
    -
    {name} + {viewingQuery ? "Viewing" : } -
    -
    - -
    - -
    +
  • ); } -function isActive( - name: string, - activeQuery?: string, -): { viewingQuery: boolean; viewingDef: boolean } { - if (!activeQuery) return { viewingDef: false, viewingQuery: false }; +function isActive(name: string, activeQuery?: string): boolean { + if (!activeQuery) return false; const lQuery = activeQuery.toLowerCase().trim(); const lName = name.toLowerCase(); const viewText = "select * from"; - const defText = "show create view"; - return { - viewingQuery: matchesDef(viewText, lName, lQuery), - viewingDef: matchesDef(defText, lName, lQuery), - }; + return matchesDef(viewText, lName, lQuery); } function matchesDef(text: string, name: string, q: string): boolean { diff --git a/packages/web/components/Views/index.module.css b/packages/web/components/Views/index.module.css index f6b8921c..51a51e2c 100644 --- a/packages/web/components/Views/index.module.css +++ b/packages/web/components/Views/index.module.css @@ -1,5 +1,5 @@ .views { - @apply text-primary my-1; + @apply text-primary mb-10; } .text { @@ -7,30 +7,27 @@ } .icon { - @apply text-2xl mr-3 text-primary rounded-full opacity-25; - - &:hover { - @apply opacity-100; - } -} - -.book { - @apply mr-0 text-xl; -} - -.bookActive { - @apply text-ld-orange opacity-60; + @apply text-2xl mr-2 text-primary rounded-full opacity-25; } .item { - @apply flex w-full pt-3 pb-2 px-6; + @apply flex w-full py-2.5 px-6; + @screen lg { @apply pr-2; } } +.item:hover .icon { + @apply opacity-100; + + &:hover { + @apply text-acc-hoverblue; + } +} + .button { - @apply flex justify-between w-full; + @apply flex justify-between items-center w-full; &:focus { @apply outline-none widget-shadow-lightblue; @@ -47,5 +44,5 @@ } .viewing { - @apply py-0 mr-4 text-ld-orange font-semibold text-sm; + @apply py-0.5 mr-4 text-ld-orange font-semibold text-sm; } diff --git a/packages/web/components/Views/index.tsx b/packages/web/components/Views/index.tsx index 0c5a9fe5..f106b96c 100644 --- a/packages/web/components/Views/index.tsx +++ b/packages/web/components/Views/index.tsx @@ -1,7 +1,6 @@ import Section from "@components/DatabaseTableNav/Section"; import Loader from "@components/Loader"; -import Tooltip from "@components/Tooltip"; -import { RowForViewsFragment } from "@gen/graphql-types"; +import { RowForSchemasFragment } from "@gen/graphql-types"; import { RefParams } from "@lib/params"; import NoViews from "./NoViews"; import ViewItem from "./ViewItem"; @@ -13,13 +12,12 @@ type ViewsProps = { }; type Props = ViewsProps & { - rows?: RowForViewsFragment[]; + rows?: RowForSchemasFragment[]; }; function Inner({ rows, params }: Props) { return (
    - {rows?.length ? (
      {rows.map(r => ( @@ -43,9 +41,11 @@ export default function Views(props: ViewsProps) { const res = useViewList(props.params); return (
      - + {res.loading ? ( + + ) : ( - + )}
      ); } diff --git a/packages/web/components/Views/mocks.ts b/packages/web/components/Views/mocks.ts index 76e37d21..a927df4a 100644 --- a/packages/web/components/Views/mocks.ts +++ b/packages/web/components/Views/mocks.ts @@ -1,5 +1,8 @@ import { MockedResponse } from "@apollo/client/testing"; -import { RowForViewsFragment, RowsForViewsDocument } from "@gen/graphql-types"; +import { + RowForSchemasFragment, + RowsForViewsDocument, +} from "@gen/graphql-types"; import chance from "@lib/chance"; const databaseParams = { @@ -12,7 +15,7 @@ export const params = { refName, }; -export const rowsForViewsFragmentMock: RowForViewsFragment[] = [ +export const rowsForViewsFragmentMock: RowForSchemasFragment[] = [ ...Array(5).keys(), ].map(() => { return { diff --git a/packages/web/components/Views/queries.ts b/packages/web/components/Views/queries.ts index 6466b8fa..2008b4fc 100644 --- a/packages/web/components/Views/queries.ts +++ b/packages/web/components/Views/queries.ts @@ -1,7 +1,7 @@ import { gql } from "@apollo/client"; export const ROWS_FOR_VIEWS = gql` - fragment RowForViews on Row { + fragment RowForSchemas on Row { columnValues { displayValue } @@ -9,7 +9,7 @@ export const ROWS_FOR_VIEWS = gql` query RowsForViews($databaseName: String!, $refName: String!) { views(databaseName: $databaseName, refName: $refName) { list { - ...RowForViews + ...RowForSchemas } } } diff --git a/packages/web/components/Views/useViewList.ts b/packages/web/components/Views/useViewList.ts index 520646fe..38c55dcd 100644 --- a/packages/web/components/Views/useViewList.ts +++ b/packages/web/components/Views/useViewList.ts @@ -1,12 +1,15 @@ import { ApolloError } from "@apollo/client"; -import { RowForViewsFragment, useRowsForViewsQuery } from "@gen/graphql-types"; +import { + RowForSchemasFragment, + useRowsForViewsQuery, +} from "@gen/graphql-types"; import { RefParams } from "@lib/params"; import { useEffect, useState } from "react"; type ReturnType = { loading: boolean; error?: ApolloError; - views?: RowForViewsFragment[]; + views?: RowForSchemasFragment[]; refetch: () => Promise; }; diff --git a/packages/web/components/pageComponents/DatabasePage/ForQuery.tsx b/packages/web/components/pageComponents/DatabasePage/ForQuery.tsx index 8941d907..16f43fe9 100644 --- a/packages/web/components/pageComponents/DatabasePage/ForQuery.tsx +++ b/packages/web/components/pageComponents/DatabasePage/ForQuery.tsx @@ -1,13 +1,11 @@ -"use client"; - import HistoryTable from "@components/HistoryTable"; +import SchemaFragment from "@components/SchemaFragment"; import SqlDataTable from "@components/SqlDataTable"; -import ViewFragment from "@components/ViewFragment"; import QueryBreadcrumbs from "@components/breadcrumbs/QueryBreadcrumbs"; import { DataTableProvider } from "@contexts/dataTable"; import { isDoltDiffTableQuery, - isShowViewFragmentQuery, + isShowSchemaFragmentQuery, } from "@lib/doltSystemTables"; import { RefParams, SqlQueryParams } from "@lib/params"; import { isMutation } from "@lib/parseSqlQuery"; @@ -39,10 +37,10 @@ function Inner({ params }: Props) { ); } - if (isShowViewFragmentQuery(params.q)) { + if (isShowSchemaFragmentQuery(params.q)) { return ( - + ); } diff --git a/packages/web/contexts/dataTable/index.tsx b/packages/web/contexts/dataTable/index.tsx index f1d68418..5041082a 100644 --- a/packages/web/contexts/dataTable/index.tsx +++ b/packages/web/contexts/dataTable/index.tsx @@ -13,7 +13,7 @@ import useContextWithError from "@hooks/useContextWithError"; import Maybe from "@lib/Maybe"; import { createCustomContext } from "@lib/createCustomContext"; import { RefParams, SqlQueryParams, TableParams } from "@lib/params"; -import { isMutation, tryTableNameForSelect } from "@lib/parseSqlQuery"; +import { isMutation, requireTableNamesForSelect } from "@lib/parseSqlQuery"; import { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; type DataTableParams = TableParams & { offset?: number }; @@ -29,6 +29,7 @@ type DataTableContextType = { foreignKeys?: ForeignKeysForDataTableFragment[]; error?: ApolloError; showingWorkingDiff: boolean; + tableNames: string[]; }; export const DataTableContext = @@ -42,6 +43,7 @@ type Props = { type TableProps = Props & { params: DataTableParams; + tableNames: string[]; }; function ProviderForTableName(props: TableProps) { @@ -49,6 +51,7 @@ function ProviderForTableName(props: TableProps) { const tableRes = useDataTableQuery({ variables: props.params, }); + const rowRes = useRowsForDataTableQuery({ variables: props.params, }); @@ -94,6 +97,7 @@ function ProviderForTableName(props: TableProps) { foreignKeys: tableRes.data?.table.foreignKeys, error: tableRes.error ?? rowRes.error, showingWorkingDiff: !!props.showingWorkingDiff, + tableNames: props.tableNames, }; }, [ loadMore, @@ -108,6 +112,7 @@ function ProviderForTableName(props: TableProps) { tableRes.error, tableRes.loading, props.showingWorkingDiff, + props.tableNames, ]); return ( @@ -123,8 +128,13 @@ export function DataTableProvider({ children, showingWorkingDiff, }: Props) { - const tableName = - "tableName" in params ? params.tableName : tryTableNameForSelect(params.q); + const tableNames = useMemo( + () => + "tableName" in params + ? [params.tableName] + : requireTableNamesForSelect(params.q), + [params], + ); const value = useMemo(() => { return { @@ -133,11 +143,12 @@ export function DataTableProvider({ loadMore: async () => {}, hasMore: false, showingWorkingDiff: !!showingWorkingDiff, + tableNames, }; - }, [params, showingWorkingDiff]); + }, [params, showingWorkingDiff, tableNames]); const isMut = "q" in params && isMutation(params.q); - if (isMut || !tableName) { + if (isMut || !tableNames.length) { return ( {children} @@ -146,7 +157,10 @@ export function DataTableProvider({ } return ( - + {children} ); diff --git a/packages/web/contexts/dataTable/queries.ts b/packages/web/contexts/dataTable/queries.ts index 65b5c7ba..d4e03234 100644 --- a/packages/web/contexts/dataTable/queries.ts +++ b/packages/web/contexts/dataTable/queries.ts @@ -5,6 +5,7 @@ export const DATA_TABLE_QUERY = gql` name isPrimaryKey type + sourceTable constraints { notNull } diff --git a/packages/web/gen/graphql-types.tsx b/packages/web/gen/graphql-types.tsx index 30eb2a66..821faace 100644 --- a/packages/web/gen/graphql-types.tsx +++ b/packages/web/gen/graphql-types.tsx @@ -56,6 +56,7 @@ export type Column = { constraints?: Maybe>; isPrimaryKey: Scalars['Boolean']['output']; name: Scalars['String']['output']; + sourceTable?: Maybe; type: Scalars['String']['output']; }; @@ -264,6 +265,8 @@ export type Query = { docOrDefaultDoc?: Maybe; docs: DocList; doltDatabaseDetails: DoltDatabaseDetails; + doltProcedures?: Maybe; + doltSchemas: RowList; hasDatabaseEnv: Scalars['Boolean']['output']; rowDiffs: RowDiffList; rows: RowList; @@ -344,6 +347,18 @@ export type QueryDocsArgs = { }; +export type QueryDoltProceduresArgs = { + databaseName: Scalars['String']['input']; + refName: Scalars['String']['input']; +}; + + +export type QueryDoltSchemasArgs = { + databaseName: Scalars['String']['input']; + refName: Scalars['String']['input']; +}; + + export type QueryRowDiffsArgs = { databaseName: Scalars['String']['input']; filterByRowType?: InputMaybe; @@ -678,9 +693,25 @@ export type TableListForSchemasQueryVariables = Exact<{ export type TableListForSchemasQuery = { __typename?: 'Query', tables: Array<{ __typename?: 'Table', _id: string, tableName: string, foreignKeys: Array<{ __typename?: 'ForeignKey', tableName: string, columnName: string, referencedTableName: string, foreignKeyColumn: Array<{ __typename?: 'ForeignKeyColumn', referencedColumnName: string, referrerColumnIndex: number }> }>, columns: Array<{ __typename?: 'Column', name: string, type: string, isPrimaryKey: boolean, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }>, indexes: Array<{ __typename?: 'Index', name: string, type: string, comment: string, columns: Array<{ __typename?: 'IndexColumn', name: string, sqlType?: string | null }> }> }> }; +export type RowsForDoltSchemasQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + refName: Scalars['String']['input']; +}>; + + +export type RowsForDoltSchemasQuery = { __typename?: 'Query', doltSchemas: { __typename?: 'RowList', list: Array<{ __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }> } }; + +export type RowsForDoltProceduresQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + refName: Scalars['String']['input']; +}>; + + +export type RowsForDoltProceduresQuery = { __typename?: 'Query', doltProcedures?: { __typename?: 'RowList', list: Array<{ __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }> } | null }; + export type RowForSqlDataTableFragment = { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }; -export type ColumnForSqlDataTableFragment = { __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string }; +export type ColumnForSqlDataTableFragment = { __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, sourceTable?: string | null }; export type SqlSelectForSqlDataTableQueryVariables = Exact<{ databaseName: Scalars['String']['input']; @@ -689,7 +720,7 @@ export type SqlSelectForSqlDataTableQueryVariables = Exact<{ }>; -export type SqlSelectForSqlDataTableQuery = { __typename?: 'Query', sqlSelect: { __typename?: 'SqlSelect', queryExecutionStatus: QueryExecutionStatus, queryExecutionMessage: string, columns: Array<{ __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string }>, rows: Array<{ __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }> } }; +export type SqlSelectForSqlDataTableQuery = { __typename?: 'Query', sqlSelect: { __typename?: 'SqlSelect', queryExecutionStatus: QueryExecutionStatus, queryExecutionMessage: string, columns: Array<{ __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, sourceTable?: string | null }>, rows: Array<{ __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }> } }; export type StatusFragment = { __typename?: 'Status', _id: string, refName: string, tableName: string, staged: boolean, status: string }; @@ -714,7 +745,7 @@ export type TableForBranchQueryVariables = Exact<{ export type TableForBranchQuery = { __typename?: 'Query', table: { __typename?: 'Table', _id: string, tableName: string, columns: Array<{ __typename?: 'Column', name: string, type: string, isPrimaryKey: boolean, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }> } }; -export type RowForViewsFragment = { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }; +export type RowForSchemasFragment = { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }; export type RowsForViewsQueryVariables = Exact<{ databaseName: Scalars['String']['input']; @@ -861,7 +892,7 @@ export type DoltDatabaseDetailsQueryVariables = Exact<{ [key: string]: never; }> export type DoltDatabaseDetailsQuery = { __typename?: 'Query', doltDatabaseDetails: { __typename?: 'DoltDatabaseDetails', isDolt: boolean, hideDoltFeatures: boolean } }; -export type ColumnForDataTableFragment = { __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }; +export type ColumnForDataTableFragment = { __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, sourceTable?: string | null, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }; export type ForeignKeyColumnForDataTableFragment = { __typename?: 'ForeignKeyColumn', referencedColumnName: string, referrerColumnIndex: number }; @@ -874,7 +905,7 @@ export type DataTableQueryVariables = Exact<{ }>; -export type DataTableQuery = { __typename?: 'Query', table: { __typename?: 'Table', _id: string, columns: Array<{ __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }>, foreignKeys: Array<{ __typename?: 'ForeignKey', tableName: string, columnName: string, referencedTableName: string, foreignKeyColumn: Array<{ __typename?: 'ForeignKeyColumn', referencedColumnName: string, referrerColumnIndex: number }> }> } }; +export type DataTableQuery = { __typename?: 'Query', table: { __typename?: 'Table', _id: string, columns: Array<{ __typename?: 'Column', name: string, isPrimaryKey: boolean, type: string, sourceTable?: string | null, constraints?: Array<{ __typename?: 'ColConstraint', notNull: boolean }> | null }>, foreignKeys: Array<{ __typename?: 'ForeignKey', tableName: string, columnName: string, referencedTableName: string, foreignKeyColumn: Array<{ __typename?: 'ForeignKeyColumn', referencedColumnName: string, referrerColumnIndex: number }> }> } }; export type RowForDataTableFragment = { __typename?: 'Row', columnValues: Array<{ __typename?: 'ColumnValue', displayValue: string }> }; @@ -1129,6 +1160,7 @@ export const ColumnForSqlDataTableFragmentDoc = gql` name isPrimaryKey type + sourceTable } `; export const StatusFragmentDoc = gql` @@ -1149,8 +1181,8 @@ export const TableWithColumnsFragmentDoc = gql` } } ${ColumnForTableListFragmentDoc}`; -export const RowForViewsFragmentDoc = gql` - fragment RowForViews on Row { +export const RowForSchemasFragmentDoc = gql` + fragment RowForSchemas on Row { columnValues { displayValue } @@ -1218,6 +1250,7 @@ export const ColumnForDataTableFragmentDoc = gql` name isPrimaryKey type + sourceTable constraints { notNull } @@ -1766,6 +1799,82 @@ export function useTableListForSchemasLazyQuery(baseOptions?: Apollo.LazyQueryHo export type TableListForSchemasQueryHookResult = ReturnType; export type TableListForSchemasLazyQueryHookResult = ReturnType; export type TableListForSchemasQueryResult = Apollo.QueryResult; +export const RowsForDoltSchemasDocument = gql` + query RowsForDoltSchemas($databaseName: String!, $refName: String!) { + doltSchemas(databaseName: $databaseName, refName: $refName) { + list { + ...RowForSchemas + } + } +} + ${RowForSchemasFragmentDoc}`; + +/** + * __useRowsForDoltSchemasQuery__ + * + * To run a query within a React component, call `useRowsForDoltSchemasQuery` and pass it any options that fit your needs. + * When your component renders, `useRowsForDoltSchemasQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useRowsForDoltSchemasQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * refName: // value for 'refName' + * }, + * }); + */ +export function useRowsForDoltSchemasQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(RowsForDoltSchemasDocument, options); + } +export function useRowsForDoltSchemasLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(RowsForDoltSchemasDocument, options); + } +export type RowsForDoltSchemasQueryHookResult = ReturnType; +export type RowsForDoltSchemasLazyQueryHookResult = ReturnType; +export type RowsForDoltSchemasQueryResult = Apollo.QueryResult; +export const RowsForDoltProceduresDocument = gql` + query RowsForDoltProcedures($databaseName: String!, $refName: String!) { + doltProcedures(databaseName: $databaseName, refName: $refName) { + list { + ...RowForSchemas + } + } +} + ${RowForSchemasFragmentDoc}`; + +/** + * __useRowsForDoltProceduresQuery__ + * + * To run a query within a React component, call `useRowsForDoltProceduresQuery` and pass it any options that fit your needs. + * When your component renders, `useRowsForDoltProceduresQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useRowsForDoltProceduresQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * refName: // value for 'refName' + * }, + * }); + */ +export function useRowsForDoltProceduresQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(RowsForDoltProceduresDocument, options); + } +export function useRowsForDoltProceduresLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(RowsForDoltProceduresDocument, options); + } +export type RowsForDoltProceduresQueryHookResult = ReturnType; +export type RowsForDoltProceduresLazyQueryHookResult = ReturnType; +export type RowsForDoltProceduresQueryResult = Apollo.QueryResult; export const SqlSelectForSqlDataTableDocument = gql` query SqlSelectForSqlDataTable($databaseName: String!, $refName: String!, $queryString: String!) { sqlSelect( @@ -1892,11 +2001,11 @@ export const RowsForViewsDocument = gql` query RowsForViews($databaseName: String!, $refName: String!) { views(databaseName: $databaseName, refName: $refName) { list { - ...RowForViews + ...RowForSchemas } } } - ${RowForViewsFragmentDoc}`; + ${RowForSchemasFragmentDoc}`; /** * __useRowsForViewsQuery__ diff --git a/packages/web/lib/doltSystemTables.test.ts b/packages/web/lib/doltSystemTables.test.ts index b847ea80..8d57fe1a 100644 --- a/packages/web/lib/doltSystemTables.test.ts +++ b/packages/web/lib/doltSystemTables.test.ts @@ -1,8 +1,8 @@ import { + isDoltDiffTableQuery, isDoltSystemTable, - isShowViewFragmentQuery, + isShowSchemaFragmentQuery, isUneditableDoltSystemTable, - isDoltDiffTableQuery, } from "./doltSystemTables"; describe("test doltSystemTables util functions", () => { @@ -24,16 +24,24 @@ describe("test doltSystemTables util functions", () => { expect(isUneditableDoltSystemTable("dolttable")).toBeFalsy(); }); - it("checks isShowViewFragmentQuery", () => { + it("checks isShowSchemaFragmentQuery", () => { expect( - isShowViewFragmentQuery("SHOW CREATE VIEW cases_by_age_range"), + isShowSchemaFragmentQuery("SHOW CREATE VIEW cases_by_age_range"), ).toBeTruthy(); expect( - isShowViewFragmentQuery("show create view `cases_by_age_range`"), + isShowSchemaFragmentQuery("show create view `cases_by_age_range`"), ).toBeTruthy(); - expect(isShowViewFragmentQuery("show view cases")).toBeFalsy(); + expect( + isShowSchemaFragmentQuery("show create event `cases_by_age_range`"), + ).toBeTruthy(); + expect( + isShowSchemaFragmentQuery("show create trigger `cases_by_age_range`"), + ).toBeTruthy(); + expect( + isShowSchemaFragmentQuery("show create table `cases_by_age_range`"), + ).toBeFalsy(); + expect(isShowSchemaFragmentQuery("show view cases")).toBeFalsy(); }); - it("checks isDoltDiffTableQuery", () => { expect( isDoltDiffTableQuery("select * from dolt_diff_tablename"), diff --git a/packages/web/lib/doltSystemTables.ts b/packages/web/lib/doltSystemTables.ts index 7c539fd9..edcbfd13 100644 --- a/packages/web/lib/doltSystemTables.ts +++ b/packages/web/lib/doltSystemTables.ts @@ -16,8 +16,8 @@ export function isUneditableDoltSystemTable(t: Maybe): boolean { return !editableSystemTables.includes(t); } -export function isShowViewFragmentQuery(q: string): boolean { - return q.toLowerCase().startsWith("show create view"); +export function isShowSchemaFragmentQuery(q: string): boolean { + return !!q.match(/show create (view|event|trigger|procedure)/gi); } export function isDoltDiffTableQuery(q: string): boolean | undefined { diff --git a/packages/web/lib/parseSqlQuery/index.test.ts b/packages/web/lib/parseSqlQuery/index.test.ts index b3316771..9b767e5f 100644 --- a/packages/web/lib/parseSqlQuery/index.test.ts +++ b/packages/web/lib/parseSqlQuery/index.test.ts @@ -1,6 +1,10 @@ +import { ColumnForDataTableFragment } from "@gen/graphql-types"; +import compareArray from "@lib/compareArray"; +import { NULL_VALUE } from "@lib/null"; import { convertToSqlWithNewCondition, convertToSqlWithOrderBy, + fallbackGetTableNamesForSelect, getQueryType, getTableName, isMultipleQueries, @@ -8,7 +12,6 @@ import { makeQueryExecutable, removeColumnFromQuery, } from "."; -import { NULL_VALUE } from "../null"; import { mutationExamples } from "./mutationExamples"; const invalidQuery = `this is not a valid query`; @@ -249,49 +252,113 @@ describe("test isMutation", () => { }); }); +const columns = [ + { + name: "id", + constraintsList: [], + isPrimaryKey: true, + type: "VARCHAR(16383)", + sourceTable: "tablename", + }, + { + name: "name", + constraintsList: [], + isPrimaryKey: true, + type: "VARCHAR(16383)", + sourceTable: "tablename", + }, + { + name: "age", + constraintsList: [], + isPrimaryKey: true, + type: "VARCHAR(16383)", + sourceTable: "tablename", + }, +]; + +const joinedColumns = [ + { + name: "id", + constraintsList: [], + isPrimaryKey: true, + type: "VARCHAR(16383)", + sourceTable: "tablename", + }, + { + name: "name", + constraintsList: [], + isPrimaryKey: true, + type: "VARCHAR(16383)", + sourceTable: "tablename", + }, + { + name: "age", + constraintsList: [], + isPrimaryKey: true, + type: "VARCHAR(16383)", + sourceTable: "tablename", + }, + { + name: "id", + constraintsList: [], + isPrimaryKey: true, + type: "VARCHAR(16383)", + sourceTable: "tablename2", + }, +]; + describe("removes column from query", () => { const tests: Array<{ desc: string; query: string; colToRemove: string; - cols: string[]; + cols: ColumnForDataTableFragment[]; expected: string; }> = [ { desc: "select query", query: "SELECT * FROM tablename", colToRemove: "name", - cols: ["id", "name"], + cols: columns.slice(0, 2), expected: "SELECT `id` FROM `tablename`", }, { desc: "select query with where clause", query: "SELECT id, name, age FROM tablename WHERE id=1", colToRemove: "id", - cols: ["id", "name", "age"], + cols: columns, expected: "SELECT `name`, `age` FROM `tablename` WHERE `id` = 1", }, { desc: "select query with where not clause with double quoted single quote", query: `SELECT id, name, age FROM tablename WHERE NOT (id=1 AND name = "MCDONALD'S")`, colToRemove: "name", - cols: ["id", "name", "age"], + cols: columns, expected: `SELECT \`id\`, \`age\` FROM \`tablename\` WHERE NOT(\`id\` = 1 AND \`name\` = "MCDONALD\\'S")`, }, { desc: "select query with where clause with escaped single quote", query: `SELECT * FROM tablename WHERE name = 'MCDONALD\\'S'`, colToRemove: "age", - cols: ["id", "name", "age"], + cols: columns, expected: `SELECT \`id\`, \`name\` FROM \`tablename\` WHERE \`name\` = 'MCDONALD\\'S'`, }, { desc: "select query with where clause with two escaped single quotes", query: `SELECT * FROM tablename WHERE name = 'MCDONALD\\'S' OR name = 'Jinky\\'s Cafe'`, colToRemove: "age", - cols: ["id", "name", "age"], + cols: columns, expected: `SELECT \`id\`, \`name\` FROM \`tablename\` WHERE \`name\` = 'MCDONALD\\'S' OR \`name\` = 'Jinky\\'s Cafe'`, }, + { + desc: "select query with join clause", + query: + "SELECT * FROM tablename, tablename2 where tablename.id = tablename2.id", + colToRemove: "name", + cols: joinedColumns, + expected: + "SELECT `tablename`.`id`, `tablename`.`age`, `tablename2`.`id` FROM `tablename`, `tablename2` WHERE `tablename`.`id` = `tablename2`.`id`", + }, ]; tests.forEach(test => { @@ -303,7 +370,7 @@ describe("removes column from query", () => { }); expect(() => - removeColumnFromQuery(invalidQuery, "age", ["id", "age"]), + removeColumnFromQuery(invalidQuery, "age", columns.slice(0, 2)), ).not.toThrowError(); }); @@ -318,7 +385,7 @@ describe("test executable query", () => { query: ` select * from tablename where col='name' - + `, }, ]; @@ -330,3 +397,53 @@ where col='name' }); }); }); + +describe("test use regex to get table names from query", () => { + const tests = [ + { + desc: "single table", + query: "select * from tablename where col='name'", + expected: ["tablename"], + }, + { + desc: "single table with where clause", + query: "select * from tablename where col='name'", + expected: ["tablename"], + }, + { + desc: "multiple tables using , to join", + query: "select * from table1, table2 where table1.id = table2.id", + expected: ["table1", "table2"], + }, + { + desc: "multiple tables using join clause", + query: "select * from table1 join table2 on table1.id = table2.id", + expected: ["table1", "table2"], + }, + { + desc: "multiple tables with table names in backticks", + query: + "select * from `table1` join `table2` on `table1`.id = `table2`.id", + expected: ["table1", "table2"], + }, + { + desc: "multiple tables with column name includes from", + query: + "select * from table1 join table2 on table1.from_commit = table2.from_commit", + expected: ["table1", "table2"], + }, + // { + // desc: "more than 2 tables", + // query: + // "select * from table1, table2, table3 where table1.id = table2.id and table2.id = table3.id", + // expected: ["table1", "table2", "table3"], + // }, + ]; + tests.forEach(test => { + it(test.desc, () => { + expect( + compareArray(fallbackGetTableNamesForSelect(test.query), test.expected), + ).toBe(true); + }); + }); +}); diff --git a/packages/web/lib/parseSqlQuery/index.ts b/packages/web/lib/parseSqlQuery/index.ts index 9f76865c..e4fa359d 100644 --- a/packages/web/lib/parseSqlQuery/index.ts +++ b/packages/web/lib/parseSqlQuery/index.ts @@ -1,3 +1,4 @@ +import { ColumnForDataTableFragment } from "@gen/graphql-types"; import Maybe from "@lib/Maybe"; import { AST, ColumnRef, OrderBy, Parser, Select } from "node-sql-parser"; import { isNullValue } from "../null"; @@ -33,7 +34,11 @@ export function getTableNames(q: string): string[] | undefined { } export function tryTableNameForSelect(q: string): Maybe { - return getTableName(q) ?? fallbackGetTableNameForSelect(q); + return getTableName(q) ?? fallbackGetTableNamesForSelect(q)[0]; +} + +export function requireTableNamesForSelect(q: string): string[] { + return getTableNames(q) ?? fallbackGetTableNamesForSelect(q); } // Extracts tableName from query @@ -44,14 +49,14 @@ export function getTableName(q?: string): Maybe { return tns[0]; } -// Uses index of "FROM" in query "SELECT [columns] FROM [tableName] ..." to extract table name -function fallbackGetTableNameForSelect(q: string): Maybe { - const splitLower = q.toLowerCase().split(/\s/); - const fromIndex = splitLower.indexOf("from"); - if (fromIndex === -1) return undefined; - const tableNameIndex = fromIndex + 1; - const tableName = q.split(/\s/)[tableNameIndex]; - return tableName.replace(/`/g, ""); +// Uses regex to match table names in query "SELECT [columns] FROM [tableName] ..." +// does not work on more than 2 tables. but better than just extract 1 table +export function fallbackGetTableNamesForSelect(query: string): string[] { + const tableNameRegex = + /\b(?:from|join)\s+`?(\w+)`?(?:\s*(?:join|,)\s+`?(\w+)`?)*\b/gi; + const matches = [...query.matchAll(tableNameRegex)]; + const tableNames = matches.flatMap(match => match.slice(1).filter(Boolean)); + return tableNames; } type Column = { @@ -67,7 +72,7 @@ export function getColumns(q: string): Columns | undefined { return ast?.columns; } -function mapColsToColumnRef(cols: string[]): Column[] { +function mapColsToColumnNames(cols: string[]): Column[] { return cols.map(c => { return { expr: { @@ -80,18 +85,34 @@ function mapColsToColumnRef(cols: string[]): Column[] { }); } +function mapColsToColumnRef( + cols: ColumnForDataTableFragment[], + isJoinClause: boolean, +): Column[] { + return cols.map(c => { + return { + expr: { + type: "column_ref", + table: isJoinClause && c.sourceTable ? c.sourceTable : null, + column: c.name, + }, + as: null, + }; + }); +} + export function convertToSql(select: Select): string { return parser.sqlify(select, opt); } // Converts query string to sql with new table name and columns -export function convertToSqlWithNewCols( +export function convertToSqlWithNewColNames( q: string, cols: string[] | "*", tableName: string, ): string { const ast = parseSelectQuery(q); - const columns = cols === "*" ? cols : mapColsToColumnRef(cols); + const columns = cols === "*" ? cols : mapColsToColumnNames(cols); if (!ast) return ""; const newAst: Select = { ...ast, @@ -102,6 +123,37 @@ export function convertToSqlWithNewCols( return convertToSql(newAst); } +// Converts query string to sql with new table name and columns +export function convertToSqlWithNewCols( + q: string, + cols: ColumnForDataTableFragment[] | "*", + tableNames?: string[], +): string { + const ast = parseSelectQuery(q); + const isJoinClause = tableNames && tableNames.length > 1; + const columns = + cols === "*" ? cols : mapColsToColumnRef(cols, !!isJoinClause); + + if (!ast) return ""; + if (!tableNames || tableNames.length === 0) { + return convertToSql({ + ...ast, + columns, + from: [{ db: null, table: null, as: null }], + where: escapeSingleQuotesInWhereObj(ast.where), + }); + } + const newAst: Select = { + ...ast, + columns, + from: tableNames.map(table => { + return { db: null, table, as: null }; + }), + where: escapeSingleQuotesInWhereObj(ast.where), + }; + return convertToSql(newAst); +} + // Adds condition to query string export function convertToSqlWithNewCondition( query: string, @@ -286,11 +338,10 @@ export function isMutation(q?: string): boolean { export function removeColumnFromQuery( q: string, colNameToRemove: string, - cols: string[], + cols: ColumnForDataTableFragment[], ): string { - const tableName = tryTableNameForSelect(q); - if (!tableName) return ""; - const newCols = cols.filter(c => c !== colNameToRemove); + const newCols = cols.filter(c => c.name !== colNameToRemove); + const tableName = getTableNames(q); return convertToSqlWithNewCols(q, newCols, tableName); } diff --git a/packages/web/lib/refetchQueries.ts b/packages/web/lib/refetchQueries.ts index 364e079d..d5f176a8 100644 --- a/packages/web/lib/refetchQueries.ts +++ b/packages/web/lib/refetchQueries.ts @@ -102,10 +102,12 @@ export const refetchSqlUpdateQueriesCacheEvict: RefetchOptions = { "docs", "commits", "status", - // "diffSummaries", - // "diffStat", - // "rowDiffs", - // "schemaDiff", + "diffSummaries", + "diffStat", + "rowDiffs", + "schemaDiff", + "doltProcedures", + "doltSchemas", ].forEach(fieldName => { cache.evict({ fieldName }); });