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
This commit is contained in:
Sameer Kolhar 2021-08-11 23:32:05 +05:30 committed by hasura-bot
parent 39c9e9f7e3
commit 3f35a9a219
17 changed files with 903 additions and 28 deletions

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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> = props => (
<>
<h4 className={styles.subheading_text}>
Indexes &nbsp; &nbsp;
<Tooltip message={tooltipMessage} />
</h4>
<IndexFieldsEditor currentTableInfo={props.tableSchema} />
</>
);
export default IndexFields;

View File

@ -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<IndexColumnsSelect>) => void;
updateIndexName: (name: string) => void;
onChangeIndexColumnsSelect: (value: ValueType<IndexColumnsSelect>) => void;
toggleIndexCheckboxState: (currentValue: boolean) => () => void;
}
const CreateIndexForm: React.FC<CreateIndexProps> = ({
indexState,
tableColumnOptions,
indexTypeOptions,
onChangeIndexColumnsSelect,
updateIndexName,
onChangeIndextypeSelect,
toggleIndexCheckboxState,
}) => (
<div className={styles.indexEditorContainer}>
<div
className={`${styles.add_mar_top_small} ${styles.add_mar_bottom} ${styles.indexEditorGrid}`}
>
<div className={styles.indexNameWidth}>
<label htmlFor="index-name" className={styles.indexCreateFormLabel}>
Index Name
</label>
{formTooltips?.indexName && (
<ToolTip message={formTooltips.indexName} />
)}
<TextInput
onChange={e => updateIndexName(e.target.value)}
value={indexState.index_name}
id="index-name"
placeholder="Input Name"
bsclass={styles.inputNameStyles}
/>
</div>
<div className={styles.modifyIndexSelectContainer}>
<label
htmlFor="index-type-select"
className={styles.indexCreateFormLabel}
>
Index Type
</label>
{formTooltips?.indexType && (
<ToolTip message={formTooltips.indexType} />
)}
<Select
options={indexTypeOptions}
className={`${styles.indexColumnSelectStyles} legacy-input-fix`}
placeholder="-- select index type --"
onChange={onChangeIndextypeSelect}
/>
</div>
<div>
<label
htmlFor="create-index-columns"
className={styles.indexCreateFormLabel}
>
Columns
</label>
{formTooltips?.indexColumns && (
<ToolTip message={formTooltips.indexColumns} />
)}
<Select
isMulti={supportedIndex?.multiColumn.includes(indexState.index_type)}
options={tableColumnOptions}
className={`${styles.indexColumnSelectStyles} legacy-input-fix`}
placeholder="-- select columns --"
onChange={onChangeIndexColumnsSelect}
/>
</div>
<div className={styles.indexOptionsContainer}>
<span className={styles.indexOption}>
<label htmlFor="index-unique" className={styles.indexCreateFormLabel}>
Unique?
</label>
{formTooltips?.unique ? (
<ToolTip message={formTooltips.unique} />
) : null}
<input
type="checkbox"
id="index-unique"
onChange={toggleIndexCheckboxState(indexState?.unique ?? false)}
checked={indexState?.unique ?? false}
className={`${styles.uniqueCheckbox} legacy-input-fix`}
/>
</span>
</div>
</div>
</div>
);
const FieldsEditor: React.FC<IndexFieldsEditorProps> = props => {
const { dispatch, currentTableInfo } = props;
const [indexState, indexStateDispatch] = useReducer(
indexStateReducer,
defaultIndexState
);
const [indexes, setIndexes] = useState<Index[]>([]);
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<IndexColumnsSelect>) => {
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<IndexColumnsSelect>) => {
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 = () => (
<CreateIndexForm
indexState={indexState}
tableColumnOptions={tableColumnOptions}
indexTypeOptions={indexTypeOptions}
onChangeIndexColumnsSelect={onChangeIndexColumnsSelect}
updateIndexName={updateIndexName}
onChangeIndextypeSelect={onChangeIndextypeSelect}
toggleIndexCheckboxState={toggleIndexCheckboxState}
/>
);
return (
<>
<div className={styles.indexesList}>
{numberOfIndexes
? indexes.sort(pkSortFn).map(indexInfo => {
const indexSql = indexInfo.index_definition_sql;
return (
<div
key={indexInfo.index_name}
className={styles.indexesListItem}
>
<Button
size="xs"
className={styles.indexRemoveBtn}
disabled={isPrimarykeyIndex(indexInfo)}
onClick={onClickRemoveIndex(indexInfo)}
>
Remove
</Button>
<b className={styles.indexListItemIndexName}>
{indexInfo.index_name}
</b>
<p className={styles.indexDef}>
{isPrimarykeyIndex(indexInfo) && <i>PRIMARY KEY, </i>}
{isUnique(indexSql) && <i>UNIQUE</i>}
<i className={styles.uppercase}>{indexInfo.index_type}</i>
<b>
<i>on</i>
</b>
<span>{getDefCols(indexSql)}</span>
</p>
</div>
);
})
: null}
</div>
{isFeatureSupported('tables.modify.indexes.edit') ? (
<ExpandableEditor
editorExpanded={editorExpanded}
property="create-index"
service="modify-table"
saveFunc={onSave}
expandButtonText={`Create ${numberOfIndexes ? ' an index' : ''}`}
collapsedLabel={() =>
`${!numberOfIndexes ? 'No Indexes Present' : ''}`
}
collapseButtonText="Cancel"
collapseCallback={resetIndexEditState}
/>
) : null}
</>
);
};
const indexFieldsEditorConnector = connect(null, mapDispatchToPropsEmpty);
type ConnectorProps = ConnectedProps<typeof indexFieldsEditorConnector>;
const IndexFieldsEditor = indexFieldsEditorConnector(FieldsEditor);
export default IndexFieldsEditor;

View File

@ -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,
};

View File

@ -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 {
<hr className="my-lg" />
</>
)}
{isFeatureSupported('tables.modify.indexes.view') ? (
<>
<IndexFields tableSchema={table} />
<hr />
</>
) : null}
{isFeatureSupported('tables.modify.triggers') && (
<>
<div className={styles.add_mar_bottom}>

View File

@ -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;
}

View File

@ -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<string, IndexType>;
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<SupportedFeaturesType>;
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<SupportedFeaturesType>) => {
export const isFeatureSupported = (
feature: Path<DeepRequired<SupportedFeaturesType>>
) => {
if (dataSource.supportedFeatures)
return get(dataSource.supportedFeatures, feature);
};
export const getSupportedDrivers = (
feature: Path<SupportedFeaturesType>
): Driver[] => {
const isEnabled = (supportedFeatures: SupportedFeaturesType) => {
return get(supportedFeatures, feature) || false;
};
return [
export const getSupportedDrivers = (feature: Path<SupportedFeaturesType>) =>
[
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';

View File

@ -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<SupportedFeaturesType> = {
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,

View File

@ -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<SupportedFeaturesType> = {
...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: {

View File

@ -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<SupportedFeaturesType> = {
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,

View File

@ -238,6 +238,9 @@ WHERE
primaryKeysInfoSql,
uniqueKeysSql,
checkConstraintsSql: undefined,
tableIndexSql: undefined,
createIndexSql: undefined,
dropIndexSql: undefined,
getFKRelations,
getReferenceOption: (option: string) => option,
getEventInvocationInfoByIDSql: undefined,

View File

@ -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<string, IndexType> = {
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<SupportedFeaturesType> = {
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,

View File

@ -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',

View File

@ -12,6 +12,7 @@ import {
getRunSqlQuery,
WhereClause,
} from '../../src/components/Common/utils/v1QueryUtils';
import { Driver } from '.';
export interface Relationship
extends Pick<BaseTable, 'table_name' | 'table_schema'> {
@ -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<ColumnCategories, string[]>;
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;