From 3f35a9a21965135c84c8f9047d222a74a3670e1c Mon Sep 17 00:00:00 2001 From: Sameer Kolhar Date: Wed, 11 Aug 2021 23:32:05 +0530 Subject: [PATCH] console: support creation of indexes on the console for postgres sources https://github.com/hasura/graphql-engine-mono/pull/358 Co-authored-by: Ikechukwu Eze <22247592+iykekings@users.noreply.github.com> GitOrigin-RevId: cd4a5073fb361fe1235f5d49a0dc9b025fc1379f --- CHANGELOG.md | 1 + console/package-lock.json | 5 + console/package.json | 3 +- .../components/Services/Data/DataActions.js | 44 +++ .../Services/Data/TableModify/IndexFields.tsx | 26 ++ .../Data/TableModify/IndexFieldsEditor.tsx | 373 ++++++++++++++++++ .../Data/TableModify/ModifyActions.js | 121 ++++++ .../Services/Data/TableModify/ModifyTable.js | 8 +- .../Data/TableModify/ModifyTable.scss | 119 +++++- console/src/dataSources/index.ts | 44 ++- .../dataSources/services/bigquery/index.tsx | 20 +- .../src/dataSources/services/citus/index.tsx | 11 +- .../src/dataSources/services/mssql/index.tsx | 13 +- .../src/dataSources/services/mysql/index.tsx | 7 +- .../dataSources/services/postgresql/index.tsx | 44 ++- .../services/postgresql/sqlUtils.ts | 67 +++- console/src/dataSources/types.ts | 25 +- 17 files changed, 903 insertions(+), 28 deletions(-) create mode 100644 console/src/components/Services/Data/TableModify/IndexFields.tsx create mode 100644 console/src/components/Services/Data/TableModify/IndexFieldsEditor.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 835de133b13..e8f3a8d955a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ (Add entries below in the order of server, console, cli, docs, others) - server: fix GraphQL type for single-row returning functions (close #7109) +- console: add support for creation of indexes for Postgres data sources ## v2.0.6 diff --git a/console/package-lock.json b/console/package-lock.json index 79b6c6c22dd..09e89e74de8 100644 --- a/console/package-lock.json +++ b/console/package-lock.json @@ -23169,6 +23169,11 @@ "utf8-byte-length": "^1.0.1" } }, + "ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==" + }, "ts-invariant": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", diff --git a/console/package.json b/console/package.json index 71d64b3cc67..9ef18cd9360 100644 --- a/console/package.json +++ b/console/package.json @@ -105,7 +105,8 @@ "sql-formatter": "2.3.3", "styled-components": "5.0.1", "styled-system": "5.1.5", - "subscriptions-transport-ws": "0.9.16" + "subscriptions-transport-ws": "0.9.16", + "ts-essentials": "^7.0.3" }, "devDependencies": { "@babel/cli": "7.13.10", diff --git a/console/src/components/Services/Data/DataActions.js b/console/src/components/Services/Data/DataActions.js index 42af456b465..fe200e71e3d 100644 --- a/console/src/components/Services/Data/DataActions.js +++ b/console/src/components/Services/Data/DataActions.js @@ -47,6 +47,10 @@ import { import { getRunSqlQuery } from '../../Common/utils/v1QueryUtils'; import { services } from '../../../dataSources/services'; import insertReducer from './TableInsertItem/InsertActions'; +import { + checkFeatureSupport, + READ_ONLY_RUN_SQL_QUERIES, +} from '../../../helpers/versionUtils'; const SET_TABLE = 'Data/SET_TABLE'; const LOAD_FUNCTIONS = 'Data/LOAD_FUNCTIONS'; @@ -1004,6 +1008,46 @@ const fetchPartitionDetails = table => { }; }; +export const fetchTableIndexDetails = tableInfo => { + return (dispatch, getState) => { + const url = Endpoints.query; + const currState = getState(); + const { currentDataSource } = currState.tables; + const { table_name: table, table_schema: schema } = tableInfo; + const query = getRunSqlQuery( + dataSource.tableIndexSql({ table, schema }), + currentDataSource, + false, + checkFeatureSupport(READ_ONLY_RUN_SQL_QUERIES) || false + ); + const options = { + credentials: globalCookiePolicy, + method: 'POST', + headers: dataHeaders(getState), + body: JSON.stringify(query), + }; + return dispatch(requestAction(url, options)).then( + data => { + try { + return JSON.parse(data?.result?.[1]?.[0] ?? '[]'); + } catch (err) { + console.error(err); + return []; + } + }, + error => { + dispatch( + showErrorNotification( + 'Error fetching indexes information', + null, + error + ) + ); + } + ); + }; +}; + /* ******************************************************* */ const dataReducer = (state = defaultState, action) => { // eslint-disable-line no-unused-vars diff --git a/console/src/components/Services/Data/TableModify/IndexFields.tsx b/console/src/components/Services/Data/TableModify/IndexFields.tsx new file mode 100644 index 00000000000..392d17ca5b7 --- /dev/null +++ b/console/src/components/Services/Data/TableModify/IndexFields.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import Tooltip from '../../../Common/Tooltip/Tooltip'; +import IndexFieldsEditor from './IndexFieldsEditor'; +import { Table } from '../../../../dataSources/types'; + +import styles from './ModifyTable.scss'; + +type Props = { + tableSchema: Table; +}; + +const tooltipMessage = + 'Indexes are used to increase query performance based on columns that are queried frequently'; + +const IndexFields: React.FC = props => ( + <> +

+ Indexes     + +

+ + +); + +export default IndexFields; diff --git a/console/src/components/Services/Data/TableModify/IndexFieldsEditor.tsx b/console/src/components/Services/Data/TableModify/IndexFieldsEditor.tsx new file mode 100644 index 00000000000..64b7050b94c --- /dev/null +++ b/console/src/components/Services/Data/TableModify/IndexFieldsEditor.tsx @@ -0,0 +1,373 @@ +import React, { useEffect, useReducer, useState } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import Select, { ValueType } from 'react-select'; + +import { Index, IndexType, Table } from '../../../../dataSources/types'; +import { Button } from '../../../Common'; +import { mapDispatchToPropsEmpty } from '../../../Common/utils/reactUtils'; +import { removeIndex, saveIndex } from './ModifyActions'; +import { showErrorNotification } from '../../Common/Notification'; +import ExpandableEditor from '../../../Common/Layout/ExpandableEditor/Editor'; +import TextInput from '../../../Common/TextInput/TextInput'; +import ToolTip from '../../../Common/Tooltip/Tooltip'; +import { dataSource, isFeatureSupported } from '../../../../dataSources'; +import { fetchTableIndexDetails } from '../DataActions'; + +import styles from './ModifyTable.scss'; + +type IndexState = { + index_name: string; + index_type: IndexType; + index_columns: string[]; + unique?: boolean; +}; + +export const defaultIndexState: IndexState = { + index_name: '', + index_type: 'btree', + index_columns: [], + unique: false, +}; + +interface UpdateIndexName { + type: 'Indexes/UPDATE_INDEX_NAME'; + data: string; +} + +interface UpdateIndexUniqueState { + type: 'Indexes/UPDATE_INDEX_UNIQUE_STATE'; + data: boolean; +} + +interface UpdateIndexType { + type: 'Indexes/UPDATE_INDEX_TYPE'; + data: IndexType; +} + +interface UpdateIndexColumns { + type: 'Indexes/UPDATE_INDEX_COLUMNS'; + data: string[]; +} + +interface ResetIndexState { + type: 'Indexes/RESET_INDEX_STATE'; +} + +type IndexStateAction = + | UpdateIndexColumns + | UpdateIndexName + | UpdateIndexType + | UpdateIndexUniqueState + | ResetIndexState; + +const indexStateReducer = ( + state: IndexState, + action: IndexStateAction +): IndexState => { + switch (action.type) { + case 'Indexes/UPDATE_INDEX_NAME': + return { + ...state, + index_name: action.data, + }; + case 'Indexes/UPDATE_INDEX_TYPE': + return { + ...state, + index_type: action.data, + }; + case 'Indexes/UPDATE_INDEX_COLUMNS': + return { + ...state, + index_columns: action.data, + }; + case 'Indexes/UPDATE_INDEX_UNIQUE_STATE': + return { + ...state, + unique: action.data, + }; + case 'Indexes/RESET_INDEX_STATE': + return defaultIndexState; + default: + return state; + } +}; + +const formTooltips = dataSource.indexFormToolTips; + +const supportedIndex = dataSource.supportedIndex; + +type IndexColumnsSelect = Record<'label' | 'value', string>; + +interface IndexFieldsEditorProps extends ConnectorProps { + currentTableInfo: Table; +} + +const isUnique = (indexSql: string) => /CREATE\s+UNIQUE/i.test(indexSql); + +const getDefCols = (indexSql: string) => + indexSql.split(/USING \w+ /)?.[1] ?? ''; + +interface CreateIndexProps { + indexState: IndexState; + tableColumnOptions: IndexColumnsSelect[]; + indexTypeOptions: Array<{ + label: string; + value: IndexType; + }>; + onChangeIndextypeSelect: (value: ValueType) => void; + updateIndexName: (name: string) => void; + onChangeIndexColumnsSelect: (value: ValueType) => void; + toggleIndexCheckboxState: (currentValue: boolean) => () => void; +} + +const CreateIndexForm: React.FC = ({ + indexState, + tableColumnOptions, + indexTypeOptions, + onChangeIndexColumnsSelect, + updateIndexName, + onChangeIndextypeSelect, + toggleIndexCheckboxState, +}) => ( +
+
+
+ + {formTooltips?.indexName && ( + + )} + updateIndexName(e.target.value)} + value={indexState.index_name} + id="index-name" + placeholder="Input Name" + bsclass={styles.inputNameStyles} + /> +
+
+ + {formTooltips?.indexType && ( + + )} + +
+
+ + + {formTooltips?.unique ? ( + + ) : null} + + +
+
+
+); + +const FieldsEditor: React.FC = props => { + const { dispatch, currentTableInfo } = props; + const [indexState, indexStateDispatch] = useReducer( + indexStateReducer, + defaultIndexState + ); + const [indexes, setIndexes] = useState([]); + + const fetchIndexes = () => { + dispatch(fetchTableIndexDetails(currentTableInfo)).then((data: Index[]) => { + setIndexes(data); + }); + }; + + useEffect(fetchIndexes, []); + + const updateIndexName = (name: string) => + indexStateDispatch({ type: 'Indexes/UPDATE_INDEX_NAME', data: name }); + + const toggleIndexCheckboxState = (currentValue: boolean) => () => + indexStateDispatch({ + type: 'Indexes/UPDATE_INDEX_UNIQUE_STATE', + data: !currentValue, + }); + + const tableColumns = currentTableInfo.columns.map( + column => column.column_name + ); + const tableColumnOptions = tableColumns.reduce( + (acc: IndexColumnsSelect[], columnName) => [ + ...acc, + { label: columnName, value: columnName }, + ], + [] + ); + + const indexTypeOptions = Object.entries(dataSource.indexTypes ?? {}).map( + ([key, value]) => ({ + label: key, + value, + }) + ); + + const onChangeIndexColumnsSelect = (value: ValueType) => { + if (value) { + indexStateDispatch({ + type: 'Indexes/UPDATE_INDEX_COLUMNS', + data: Array.isArray(value) + ? value.map(val => val.value).slice(0, 32) + : [(value as IndexColumnsSelect).value], + }); + } + }; + const onChangeIndextypeSelect = (value: ValueType) => { + if (value) { + indexStateDispatch({ + type: 'Indexes/UPDATE_INDEX_TYPE', + data: (value as IndexColumnsSelect).value as IndexType, + }); + } + }; + + const resetIndexEditState = () => + indexStateDispatch({ type: 'Indexes/RESET_INDEX_STATE' }); + + const onSave = (toggleEditor: () => void) => { + if ( + !indexState.index_name || + !indexState.index_columns?.length || + !indexState.index_type + ) { + dispatch( + showErrorNotification( + 'Some Required Fields are Empty', + 'Index Name, Index Columns and Index Type are all required fields' + ) + ); + return; + } + const successCb = () => { + fetchIndexes(); + toggleEditor(); + }; + dispatch(saveIndex(indexState, successCb)); + }; + + const onClickRemoveIndex = (indexInfo: Index) => () => + dispatch(removeIndex(indexInfo, fetchIndexes)); + + const isPrimarykeyIndex = (a: Index) => + a.index_name === currentTableInfo.primary_key?.constraint_name; + + const pkSortFn = (a: Index, b: Index) => + Number(isPrimarykeyIndex(b)) - Number(isPrimarykeyIndex(a)); + + const numberOfIndexes = indexes.length; + const editorExpanded = () => ( + + ); + + return ( + <> +
+ {numberOfIndexes + ? indexes.sort(pkSortFn).map(indexInfo => { + const indexSql = indexInfo.index_definition_sql; + return ( +
+ + + {indexInfo.index_name} + +

+ {isPrimarykeyIndex(indexInfo) && PRIMARY KEY, } + {isUnique(indexSql) && UNIQUE} + {indexInfo.index_type} + + on + + {getDefCols(indexSql)} +

+
+ ); + }) + : null} +
+ {isFeatureSupported('tables.modify.indexes.edit') ? ( + + `${!numberOfIndexes ? 'No Indexes Present' : ''}` + } + collapseButtonText="Cancel" + collapseCallback={resetIndexEditState} + /> + ) : null} + + ); +}; + +const indexFieldsEditorConnector = connect(null, mapDispatchToPropsEmpty); +type ConnectorProps = ConnectedProps; +const IndexFieldsEditor = indexFieldsEditorConnector(FieldsEditor); + +export default IndexFieldsEditor; diff --git a/console/src/components/Services/Data/TableModify/ModifyActions.js b/console/src/components/Services/Data/TableModify/ModifyActions.js index fb2ba741cf4..54d5ada7529 100644 --- a/console/src/components/Services/Data/TableModify/ModifyActions.js +++ b/console/src/components/Services/Data/TableModify/ModifyActions.js @@ -2077,6 +2077,125 @@ export const setViewCustomColumnNames = ( ); }; +const saveIndex = (indexInfo, successCb, errorCb) => (dispatch, getState) => { + if (!indexInfo) { + return; + } + + if (!dataSource.createIndexSql && !dataSource.dropIndexSql) { + // ERROR: this datasource does not support creation/deletion of indexes + return; + } + + if ( + !indexInfo?.index_name?.trim() || + !indexInfo?.index_columns?.length || + !indexInfo?.index_type + ) { + dispatch( + showErrorNotification( + 'Some required fields are missing', + 'Index Name, Index Columns and Index Type are all required fields' + ) + ); + return; + } + + const { currentSchema, currentTable, currentDataSource } = getState().tables; + const upQueries = []; + const downQueries = []; + + const upSql = dataSource.createIndexSql({ + table: { schema: currentSchema, name: currentTable }, + columns: indexInfo?.index_columns, + indexName: indexInfo?.index_name?.trim(), + indexType: indexInfo?.index_type, + unique: indexInfo?.unique, + }); + const downSql = dataSource.dropIndexSql(indexInfo?.index_name); + + upQueries.push(getRunSqlQuery(upSql, currentDataSource)); + downQueries.push(getRunSqlQuery(downSql, currentDataSource)); + + const migrationName = `create_index_${indexInfo?.index_name || ''}`; + const requestMsg = 'Creating index....'; + const successMsg = `Created index ${indexInfo?.index_name} successfully`; + const errorMsg = 'Failed to create index'; + + const customOnSuccess = () => successCb?.(); + + const customOnError = () => errorCb?.(); + + makeMigrationCall( + dispatch, + getState, + upQueries, + downQueries, + migrationName, + customOnSuccess, + customOnError, + requestMsg, + successMsg, + errorMsg + ); +}; + +const removeIndex = (indexInfo, successCb, errorCb) => (dispatch, getState) => { + if (!indexInfo) { + return; + } + + if (!dataSource.createIndexSql && !dataSource.dropIndexSql) { + // ERROR: this datasource does not support creation/deletion of indexes + return; + } + + const removeConfirmation = getConfirmation( + `You want to remove the index: ${indexInfo?.index_name || ''}` + ); + if (!removeConfirmation) { + return; + } + + const { currentTable, currentSchema, currentDataSource } = getState().tables; + const upQueries = []; + const downQueries = []; + + const upSql = dataSource.dropIndexSql(indexInfo?.index_name); + const downSql = dataSource.createIndexSql({ + indexName: indexInfo?.index_name, + indexType: indexInfo?.index_type, + table: { schema: currentSchema, name: currentTable }, + columns: indexInfo?.index_columns, + unique: indexInfo?.unique, + }); + + upQueries.push(getRunSqlQuery(upSql, currentDataSource)); + downQueries.push(getRunSqlQuery(downSql, currentDataSource)); + + const migrationName = `drop_index_${indexInfo?.index_name || 'indexName'}`; + const requestMsg = 'Removing index....'; + const successMsg = 'Removed index successfully'; + const errorMsg = 'Failed to remove index'; + + const customOnSuccess = () => successCb?.(); + + const customOnError = () => errorCb?.(); + + makeMigrationCall( + dispatch, + getState, + upQueries, + downQueries, + migrationName, + customOnSuccess, + customOnError, + requestMsg, + successMsg, + errorMsg + ); +}; + export { FETCH_COLUMN_TYPE_CASTS, FETCH_COLUMN_TYPE_CASTS_FAIL, @@ -2138,4 +2257,6 @@ export { modifyRootFields, setCheckConstraints, modifyTableCustomName, + saveIndex, + removeIndex, }; diff --git a/console/src/components/Services/Data/TableModify/ModifyTable.js b/console/src/components/Services/Data/TableModify/ModifyTable.js index 26bdb555568..9ae900ce4ec 100644 --- a/console/src/components/Services/Data/TableModify/ModifyTable.js +++ b/console/src/components/Services/Data/TableModify/ModifyTable.js @@ -51,6 +51,7 @@ import { RightContainer } from '../../../Common/Layout/RightContainer'; import { NotSupportedNote } from '../../../Common/NotSupportedNote'; import ConnectedComputedFields from './ComputedFields'; import FeatureDisabled from '../FeatureDisabled'; +import IndexFields from './IndexFields'; import PartitionInfo from './PartitionInfo'; class ModifyTable extends React.Component { @@ -311,7 +312,12 @@ class ModifyTable extends React.Component {
)} - + {isFeatureSupported('tables.modify.indexes.view') ? ( + <> + +
+ + ) : null} {isFeatureSupported('tables.modify.triggers') && ( <>
diff --git a/console/src/components/Services/Data/TableModify/ModifyTable.scss b/console/src/components/Services/Data/TableModify/ModifyTable.scss index 09c68aed64c..4fca4390747 100644 --- a/console/src/components/Services/Data/TableModify/ModifyTable.scss +++ b/console/src/components/Services/Data/TableModify/ModifyTable.scss @@ -72,6 +72,121 @@ hr { // overflow: auto; } +.modifyIndexEditorExpand { + display: flex; + flex-direction: row; + align-items: center; +} + +.modifyIndexSelectContainer { + width: 100%; + max-width: 280px; + &:first-child { + margin-right: 18px; + } +} + +.indexEditorContainer { + display: flex; + flex-direction: column; +} +.indexEditorGrid { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 20px; + align-items: center; + grid-template-rows: 1fr 1fr max-content; +} + +.selectIndexMenuHeader { + display: flex; + flex-direction: row; + width: 100%; + align-items: center; +} + +.indexDef { + display: grid; + grid-auto-flow: column; + column-gap: 8px; +} + +.indexesList { + margin-bottom: 20px; +} + +.indexesListItem { + display: grid; + grid-template-columns: repeat(3, max-content); + grid-column-gap: 8px; + align-items: baseline; + &:not(:last-child) { + margin-bottom: 8px; + } + & > p { + margin-bottom: 0; + } +} + +.indexRemoveBtn { + user-select: none; + color: rgb(206, 26, 26); + margin-right: 12px; + &:hover { + cursor: pointer; + } + &:disabled { + cursor: not-allowed; + color: unset; + } +} + +.uppercase { + text-transform: uppercase; +} + +.indexCreateFormLabel { + font-weight: 600; + margin-bottom: 8px; +} + +.indexOptionsContainer { + width: 50%; + display: flex; + grid-row: 3; + flex-direction: row; + align-items: baseline; + justify-content: space-between; +} + +.indexOption { + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + input[type='checkbox'] { + margin-left: 16px; + margin-top: 0px !important; + width: 15px; + height: 15px; + } +} + +.inputNameStyles { + margin-top: 8px; + height: 38px; +} + +.indexColumnSelectStyles { + margin-top: 8px; + caret-color: transparent; +} + +.uniqueCheckbox { + outline: 0; + user-select: none; +} + .readOnly { padding: 8px 12px; border-radius: 4px; @@ -111,7 +226,3 @@ hr { .paddingTopSm { padding-top: 10px !important; } - -.max_width { - max-width: 200px; -} \ No newline at end of file diff --git a/console/src/dataSources/index.ts b/console/src/dataSources/index.ts index 37b45ad829a..959bb556dce 100644 --- a/console/src/dataSources/index.ts +++ b/console/src/dataSources/index.ts @@ -1,5 +1,6 @@ /* eslint-disable import/no-mutable-exports */ import { useState, useEffect } from 'react'; +import { DeepRequired } from 'ts-essentials'; import { Path, get } from '../components/Common/utils/tsUtils'; import { services } from './services'; @@ -9,6 +10,7 @@ import { ComputedField, TableColumn, FrequentlyUsedColumn, + IndexType, PermissionColumnCategories, SupportedFeaturesType, generateTableRowRequestType, @@ -19,6 +21,7 @@ import { GenerateDeleteRowRequest, GenerateBulkDeleteRowRequest, ViolationActions, + IndexFormTips as IndexFormToolTips, } from './types'; import { PGFunction, FunctionState } from './services/postgresql/types'; import { Operations } from './common'; @@ -333,6 +336,21 @@ export interface DataSourcesAPI { schemas: string[]; tables: Table[]; }) => string; + tableIndexSql?: (options: { schema: string; table: string }) => string; + createIndexSql?: (indexInfo: { + indexName: string; + indexType: IndexType; + table: QualifiedTable; + columns: string[]; + unique?: boolean; + }) => string; + dropIndexSql?: (indexName: string) => string; + indexFormToolTips?: IndexFormToolTips; + indexTypes?: Record; + supportedIndex?: { + multiColumn: string[]; + singleColumn: string[]; + }; getFKRelations: (options: { schemas: string[]; tables: Table[] }) => string; getReferenceOption: (opt: string) => string; deleteFunctionSql?: ( @@ -352,7 +370,7 @@ export interface DataSourcesAPI { viewsSupported: boolean; // use null, if all operators are supported supportedColumnOperators: string[] | null; - supportedFeatures?: SupportedFeaturesType; + supportedFeatures?: DeepRequired; violationActions: ViolationActions[]; defaultRedirectSchema?: string; generateInsertRequest?: () => generateInsertRequestType; @@ -366,27 +384,25 @@ export interface DataSourcesAPI { export let currentDriver: Driver = 'postgres'; export let dataSource: DataSourcesAPI = services[currentDriver || 'postgres']; -export const isFeatureSupported = (feature: Path) => { +export const isFeatureSupported = ( + feature: Path> +) => { if (dataSource.supportedFeatures) return get(dataSource.supportedFeatures, feature); }; -export const getSupportedDrivers = ( - feature: Path -): Driver[] => { - const isEnabled = (supportedFeatures: SupportedFeaturesType) => { - return get(supportedFeatures, feature) || false; - }; - - return [ +export const getSupportedDrivers = (feature: Path) => + [ PGSupportedFeatures, MssqlSupportedFeatures, BigQuerySupportedFeatures, CitusQuerySupportedFeatures, - ] - .filter(d => isEnabled(d)) - .map(d => d.driver.name) as Driver[]; -}; + ].reduce((driverList: Driver[], supportedFeaturesObj) => { + if (get(supportedFeaturesObj, feature)) { + return [...driverList, supportedFeaturesObj.driver.name]; + } + return driverList; + }, []); class DataSourceChangedEvent extends Event { static type = 'data-source-changed'; diff --git a/console/src/dataSources/services/bigquery/index.tsx b/console/src/dataSources/services/bigquery/index.tsx index 828517afa95..7cbfe901a2f 100644 --- a/console/src/dataSources/services/bigquery/index.tsx +++ b/console/src/dataSources/services/bigquery/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { DeepRequired } from 'ts-essentials'; import { DataSourcesAPI } from '../..'; import { QualifiedTable } from '../../../metadata/types'; import { @@ -78,9 +79,12 @@ export const isJsonColumn = (column: BaseTableColumn): boolean => { return column.data_type_name === 'json' || column.data_type_name === 'jsonb'; }; -export const supportedFeatures: SupportedFeaturesType = { +export const supportedFeatures: DeepRequired = { driver: { name: 'bigquery', + fetchVersion: { + enabled: false, + }, }, schemas: { create: { @@ -93,6 +97,8 @@ export const supportedFeatures: SupportedFeaturesType = { tables: { create: { enabled: false, + frequentlyUsedColumns: false, + columnTypeSelector: false, }, browse: { enabled: true, @@ -103,11 +109,18 @@ export const supportedFeatures: SupportedFeaturesType = { enabled: false, }, modify: { + editableTableName: false, + readOnly: false, + comments: { + view: false, + edit: false, + }, enabled: false, columns: { view: false, edit: false, graphqlFieldName: false, + frequentlyUsedColumns: false, }, computedFields: false, primaryKeys: { @@ -127,6 +140,10 @@ export const supportedFeatures: SupportedFeaturesType = { view: false, edit: false, }, + indexes: { + view: false, + edit: false, + }, customGqlRoot: false, setAsEnum: false, untrack: false, @@ -135,6 +152,7 @@ export const supportedFeatures: SupportedFeaturesType = { relationships: { enabled: true, track: false, + remoteRelationships: false, }, permissions: { enabled: true, diff --git a/console/src/dataSources/services/citus/index.tsx b/console/src/dataSources/services/citus/index.tsx index a1e2bc0b86e..117ae4bf8b9 100644 --- a/console/src/dataSources/services/citus/index.tsx +++ b/console/src/dataSources/services/citus/index.tsx @@ -1,3 +1,5 @@ +import { DeepRequired } from 'ts-essentials'; + import { DataSourcesAPI } from '../..'; import { getFetchTablesListQuery, schemaListSql } from './sqlUtils'; import { @@ -14,10 +16,13 @@ import { supportedFeatures as PgSupportedFeatures, } from '../postgresql'; -export const supportedFeatures: SupportedFeaturesType = { +export const supportedFeatures: DeepRequired = { ...PgSupportedFeatures, driver: { name: 'citus', + fetchVersion: { + enabled: false, + }, }, tables: { ...(PgSupportedFeatures?.tables ?? {}), @@ -35,6 +40,10 @@ export const supportedFeatures: SupportedFeaturesType = { setAsEnum: false, untrack: true, delete: true, + indexes: { + edit: false, + view: false, + }, }, }, events: { diff --git a/console/src/dataSources/services/mssql/index.tsx b/console/src/dataSources/services/mssql/index.tsx index 426829f382d..ca78c50efb5 100644 --- a/console/src/dataSources/services/mssql/index.tsx +++ b/console/src/dataSources/services/mssql/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { DeepRequired } from 'ts-essentials'; import { DataSourcesAPI } from '../..'; import { QualifiedTable } from '../../../metadata/types'; import { @@ -96,9 +97,12 @@ const violationActions: ViolationActions[] = [ 'set default', ]; -export const supportedFeatures: SupportedFeaturesType = { +export const supportedFeatures: DeepRequired = { driver: { name: 'mssql', + fetchVersion: { + enabled: false, + }, }, schemas: { create: { @@ -112,6 +116,7 @@ export const supportedFeatures: SupportedFeaturesType = { create: { enabled: true, frequentlyUsedColumns: false, + columnTypeSelector: false, }, browse: { enabled: true, @@ -122,6 +127,7 @@ export const supportedFeatures: SupportedFeaturesType = { enabled: false, }, modify: { + editableTableName: false, enabled: true, columns: { view: true, @@ -148,6 +154,10 @@ export const supportedFeatures: SupportedFeaturesType = { view: true, edit: false, }, + indexes: { + view: false, + edit: false, + }, customGqlRoot: false, setAsEnum: false, untrack: true, @@ -160,6 +170,7 @@ export const supportedFeatures: SupportedFeaturesType = { relationships: { enabled: true, track: true, + remoteRelationships: false, }, permissions: { enabled: true, diff --git a/console/src/dataSources/services/mysql/index.tsx b/console/src/dataSources/services/mysql/index.tsx index b31501b008b..0f38e8f206d 100644 --- a/console/src/dataSources/services/mysql/index.tsx +++ b/console/src/dataSources/services/mysql/index.tsx @@ -181,9 +181,9 @@ export const mysql: DataSourcesAPI = { getEstimateCountQuery: (schema: string, table: string) => { return ` SELECT - TABLE_ROWS + TABLE_ROWS FROM - INFORMATION_SCHEMA.TABLES + INFORMATION_SCHEMA.TABLES WHERE information_schema.\`TABLES\`.\`TABLE_NAME\` = "${table}" AND information_schema.\`TABLES\`.\`TABLE_SCHEMA\` = ${schema}; @@ -238,6 +238,9 @@ WHERE primaryKeysInfoSql, uniqueKeysSql, checkConstraintsSql: undefined, + tableIndexSql: undefined, + createIndexSql: undefined, + dropIndexSql: undefined, getFKRelations, getReferenceOption: (option: string) => option, getEventInvocationInfoByIDSql: undefined, diff --git a/console/src/dataSources/services/postgresql/index.tsx b/console/src/dataSources/services/postgresql/index.tsx index 5fd28be5383..828bc7aed4a 100644 --- a/console/src/dataSources/services/postgresql/index.tsx +++ b/console/src/dataSources/services/postgresql/index.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import { DeepRequired } from 'ts-essentials'; + import { Table, TableColumn, @@ -6,6 +8,7 @@ import { SupportedFeaturesType, BaseTableColumn, ViolationActions, + IndexType, } from '../../types'; import { QUERY_TYPES, Operations } from '../../common'; import { PGFunction } from './types'; @@ -62,6 +65,9 @@ import { deleteFunctionSql, getEventInvocationInfoByIDSql, getDatabaseInfo, + tableIndexSql, + getCreateIndexSql, + getDropIndexSql, getTableInfo, getDatabaseVersionSql, } from './sqlUtils'; @@ -513,7 +519,31 @@ const permissionColumnDataTypes = { user_defined: [], // default for all other types }; -export const supportedFeatures: SupportedFeaturesType = { +const indexFormToolTips = { + unique: + 'Causes the system to check for duplicate values in the table when the index is created (if data already exist) and each time data is added', + indexName: + 'The name of the index to be created. No schema name can be included here; the index is always created in the same schema as its parent table', + indexType: + 'Only B-Tree, GiST, GIN and BRIN support multi-column indexes on PostgreSQL', + indexColumns: + 'In PostgreSQL, at most 32 fields can be provided while creating an index', +}; + +const indexTypes: Record = { + BRIN: 'brin', + 'SP-GIST': 'spgist', + GiST: 'gist', + HASH: 'hash', + 'B-Tree': 'btree', + GIN: 'gin', +}; + +const supportedIndex = { + multiColumn: ['brin', 'gist', 'btree', 'gin'], + singleColumn: ['hash', 'spgist'], +}; +export const supportedFeatures: DeepRequired = { driver: { name: 'postgres', fetchVersion: { @@ -537,11 +567,13 @@ export const supportedFeatures: SupportedFeaturesType = { browse: { enabled: true, aggregation: true, + customPagination: false, }, insert: { enabled: true, }, modify: { + readOnly: false, enabled: true, editableTableName: true, comments: { @@ -572,6 +604,10 @@ export const supportedFeatures: SupportedFeaturesType = { view: true, edit: true, }, + indexes: { + view: true, + edit: true, + }, customGqlRoot: true, setAsEnum: true, untrack: true, @@ -724,6 +760,12 @@ export const postgres: DataSourcesAPI = { deleteFunctionSql, getEventInvocationInfoByIDSql, getDatabaseInfo, + tableIndexSql, + createIndexSql: getCreateIndexSql, + dropIndexSql: getDropIndexSql, + indexFormToolTips, + indexTypes, + supportedIndex, getTableInfo, operators, generateTableRowRequest, diff --git a/console/src/dataSources/services/postgresql/sqlUtils.ts b/console/src/dataSources/services/postgresql/sqlUtils.ts index 2e6569dbc8f..808040b1ea5 100644 --- a/console/src/dataSources/services/postgresql/sqlUtils.ts +++ b/console/src/dataSources/services/postgresql/sqlUtils.ts @@ -1,4 +1,4 @@ -import { Table, FrequentlyUsedColumn } from '../../types'; +import { Table, FrequentlyUsedColumn, IndexType } from '../../types'; import { isColTypeString } from '.'; import { FunctionState } from './types'; import { QualifiedTable } from '../../../metadata/types'; @@ -1046,6 +1046,71 @@ SELECT n.nspname::text AS table_schema, ) AS info; `; +export const tableIndexSql = (options: { schema: string; table: string }) => ` + SELECT + COALESCE( + json_agg( + row_to_json(info) + ), + '[]' :: JSON + ) AS indexes + FROM + ( + SELECT + t.relname as table_name, + i.relname as index_name, + it.table_schema as table_schema, + am.amname as index_type, + array_agg(DISTINCT a.attname) as index_columns, + pi.indexdef as index_definition_sql + FROM + pg_class t, + pg_class i, + pg_index ix, + pg_attribute a, + information_schema.tables it, + pg_am am, + pg_indexes pi + WHERE + t.oid = ix.indrelid + and i.oid = ix.indexrelid + and a.attrelid = t.oid + and a.attnum = ANY(ix.indkey) + and t.relkind = 'r' + and pi.indexname = i.relname + and t.relname = '${options.table}' + and it.table_schema = '${options.schema}' + and am.oid = i.relam + GROUP BY + t.relname, + i.relname, + it.table_schema, + am.amname, + pi.indexdef + ORDER BY + t.relname, + i.relname + ) as info; + `; + +export const getCreateIndexSql = (indexObj: { + indexName: string; + indexType: IndexType; + table: QualifiedTable; + columns: string[]; + unique?: boolean; +}) => { + const { indexName, indexType, table, columns, unique = false } = indexObj; + + return ` + CREATE ${unique ? 'UNIQUE' : ''} INDEX "${indexName}" on + "${table.schema}"."${table.name}" using ${indexType} (${columns.join(', ')}); +`; +}; + +export const getDropIndexSql = (indexName: string) => + `DROP INDEX IF EXISTS "${indexName}"`; + export const frequentlyUsedColumns: FrequentlyUsedColumn[] = [ { name: 'id', diff --git a/console/src/dataSources/types.ts b/console/src/dataSources/types.ts index 0343a723492..90f7f5c1170 100644 --- a/console/src/dataSources/types.ts +++ b/console/src/dataSources/types.ts @@ -12,6 +12,7 @@ import { getRunSqlQuery, WhereClause, } from '../../src/components/Common/utils/v1QueryUtils'; +import { Driver } from '.'; export interface Relationship extends Pick { @@ -90,6 +91,24 @@ export type ComputedField = { comment: string | null; }; +export type IndexType = 'btree' | 'hash' | 'gin' | 'gist' | 'spgist' | 'brin'; + +export type Index = { + table_name: string; + table_schema: string; + index_name: string; + index_type: IndexType; + index_columns: string[]; + index_definition_sql: string; +}; + +export type IndexFormTips = { + unique: string; + indexName: string; + indexColumns: string; + indexType: string; +}; + export type Schema = { schema_name: string; }; @@ -214,7 +233,7 @@ export type PermissionColumnCategories = Record; export type SupportedFeaturesType = { driver: { - name: string; + name: Driver; fetchVersion?: { enabled: boolean; }; @@ -273,6 +292,10 @@ export type SupportedFeaturesType = { view: boolean; edit: boolean; }; + indexes?: { + view: boolean; + edit: boolean; + }; customGqlRoot?: boolean; setAsEnum?: boolean; untrack?: boolean;