console: filter out partitions from track table list and display partition info

### Description
Resolves #1128

### Changelist
- [x] Removed partitions from list of untracked tables (clean up awaits)
- [x] Display table definition at modify table like that of `psql \d+ tableName`
- [x] Fix broken console error when reloading console on `Modify` and `Relationships` tab for any other schema than default redirect schema.
- [x] Fetch table partition info only at /table/modify

### Screenshots
<img width="700" alt="Screenshot 2021-05-04 at 12 57 30" src="https://user-images.githubusercontent.com/9019397/116993856-4c6c2000-acd8-11eb-8a61-cd2b45d6e7ac.png">

### Changelog
- [x] Console

Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com>
Co-authored-by: Aleksandra Sikora <9019397+beerose@users.noreply.github.com>
GitOrigin-RevId: 3a6e527839daf52af101c2ce1803eefba600d29e
This commit is contained in:
Ikechukwu Eze 2021-05-04 12:19:40 +01:00 committed by hasura-bot
parent f015234ef6
commit 512c3008b6
15 changed files with 248 additions and 37 deletions

View File

@ -4,6 +4,7 @@
(Add entries below in the order of: server, console, cli, docs, others) (Add entries below in the order of: server, console, cli, docs, others)
- console: filter out partitions from track table list and display partition info
## v2.0.0-alpha.10 ## v2.0.0-alpha.10

View File

@ -1334,6 +1334,10 @@ code {
font-style: normal; font-style: normal;
} }
.fontSizeSm {
font-size: 13px !important;
}
.cursorPointer { .cursorPointer {
cursor: pointer; cursor: pointer;
} }

View File

@ -200,6 +200,7 @@ const loadSchema = configOptions => {
), ),
], ],
}; };
if (dataSource?.checkConstraintsSql) { if (dataSource?.checkConstraintsSql) {
body.args.push( body.args.push(
getRunSqlQuery( getRunSqlQuery(
@ -356,6 +357,7 @@ const fetchDataInit = (source, driver) => (dispatch, getState) => {
schemaList, schemaList,
}); });
let newSchema = ''; let newSchema = '';
const { locationBeforeTransitions } = getState().routing;
if (schemaList.length) { if (schemaList.length) {
newSchema = newSchema =
dataSource.defaultRedirectSchema && dataSource.defaultRedirectSchema &&
@ -363,6 +365,10 @@ const fetchDataInit = (source, driver) => (dispatch, getState) => {
? dataSource.defaultRedirectSchema ? dataSource.defaultRedirectSchema
: schemaList.sort(Intl.Collator().compare)[0]; : schemaList.sort(Intl.Collator().compare)[0];
} }
if (
locationBeforeTransitions &&
!locationBeforeTransitions.pathname.includes('tables')
)
dispatch({ type: UPDATE_CURRENT_SCHEMA, currentSchema: newSchema }); dispatch({ type: UPDATE_CURRENT_SCHEMA, currentSchema: newSchema });
return dispatch(updateSchemaInfo()); // TODO return dispatch(updateSchemaInfo()); // TODO
}, },
@ -916,6 +922,52 @@ const fetchColumnTypeInfo = () => {
}; };
}; };
const fetchPartitionDetails = table => {
return (dispatch, getState) => {
const url = Endpoints.query;
const currState = getState();
const { currentDataSource } = currState.tables;
const query = getRunSqlQuery(
dataSource.getPartitionDetailsSql(table.table_name, table.table_schema),
currentDataSource,
false,
true
);
const options = {
credentials: globalCookiePolicy,
method: 'POST',
headers: dataHeaders(getState),
body: JSON.stringify(query),
};
return dispatch(requestAction(url, options)).then(
data => {
try {
const partitions = data.result.slice(1).map(row => ({
parent_schema: row[0],
parent_table: row[1],
partition_name: row[2],
partition_schema: row[3],
partition_def: row[4],
partition_key: row[5],
}));
return partitions;
} catch (err) {
console.log(err);
}
},
error => {
dispatch(
showErrorNotification(
'Error fetching partition information',
null,
error
)
);
}
);
};
};
/* ******************************************************* */ /* ******************************************************* */
const dataReducer = (state = defaultState, action) => { const dataReducer = (state = defaultState, action) => {
// eslint-disable-line no-unused-vars // eslint-disable-line no-unused-vars
@ -1114,4 +1166,5 @@ export {
fetchAdditionalColumnsInfo, fetchAdditionalColumnsInfo,
SET_FILTER_SCHEMA, SET_FILTER_SCHEMA,
SET_FILTER_TABLES, SET_FILTER_TABLES,
fetchPartitionDetails,
}; };

View File

@ -55,7 +55,8 @@ const trackAllItems = (sql, isMigration, migrationName, source, driver) => (
const objects = parseCreateSQL(sql, driver); const objects = parseCreateSQL(sql, driver);
const changes = []; const changes = [];
objects.forEach(({ type, name, schema }) => { objects.forEach(({ type, name, schema, isPartition }) => {
if (isPartition) return;
let req = {}; let req = {};
if (type === 'function') { if (type === 'function') {
req = getTrackFunctionQuery(name, schema, source, {}, driver); req = getTrackFunctionQuery(name, schema, source, {}, driver);

View File

@ -17,41 +17,25 @@ const getDefaultSchema = driver => {
if (driver === 'mssql') return 'dbo'; if (driver === 'mssql') return 'dbo';
}; };
/**
* parses create table|function|view sql
* @param {string} sql
* @param {typeof currentDriver} [driver=currentDriver]
* @return {Array<{type: "table"|"function"|"view", schema: string, table: string, isPartition: boolean}>}
*/
export const parseCreateSQL = (sql, driver = currentDriver) => { export const parseCreateSQL = (sql, driver = currentDriver) => {
const _objects = []; const _objects = [];
const regExp = services[driver].createSQLRegex; const regExp = services[driver].createSQLRegex;
for (const result of sql.matchAll(regExp)) {
const matches = sql.match(new RegExp(regExp, 'gmi')); const { type, schema, table, tableWithSchema, partition } =
if (matches) { result.groups ?? {};
matches.forEach(element => { if (!type || !(table || tableWithSchema)) continue;
const itemMatch = element.match(new RegExp(regExp, 'i')); _objects.push({
type: type.toLowerCase(),
if (itemMatch && itemMatch.length === 6) { schema: getSQLValue(schema || getDefaultSchema(driver)),
const _object = {}; name: getSQLValue(table || tableWithSchema),
isPartition: !!partition,
const type = itemMatch[1];
// If group 5 is undefined, use group 3 and 4 for schema and table respectively
// If group 5 is present, use group 5 for table name using public schema.
let name;
let schema;
if (itemMatch[5]) {
name = itemMatch[5];
schema = getDefaultSchema(driver);
} else {
name = itemMatch[4];
schema = itemMatch[3];
}
_object.type = type.toLowerCase();
_object.name = getSQLValue(name);
_object.schema = getSQLValue(schema);
_objects.push(_object);
}
}); });
} }
return _objects; return _objects;
}; };

View File

@ -49,6 +49,7 @@ import { RightContainer } from '../../../Common/Layout/RightContainer';
import { NotSupportedNote } from '../../../Common/NotSupportedNote'; import { NotSupportedNote } from '../../../Common/NotSupportedNote';
import ConnectedComputedFields from './ComputedFields'; import ConnectedComputedFields from './ComputedFields';
import FeatureDisabled from '../FeatureDisabled'; import FeatureDisabled from '../FeatureDisabled';
import PartitionInfo from './PartitionInfo';
class ModifyTable extends React.Component { class ModifyTable extends React.Component {
componentDidMount() { componentDidMount() {
@ -191,6 +192,7 @@ class ModifyTable extends React.Component {
dispatch={dispatch} dispatch={dispatch}
/> />
<EnumTableModifyWarning isEnum={table.is_enum} /> <EnumTableModifyWarning isEnum={table.is_enum} />
<h4 className={styles.subheading_text}>Columns</h4> <h4 className={styles.subheading_text}>Columns</h4>
<ColumnEditorList <ColumnEditorList
validTypeCasts={validTypeCasts} validTypeCasts={validTypeCasts}
@ -269,6 +271,11 @@ class ModifyTable extends React.Component {
dispatch={dispatch} dispatch={dispatch}
/> />
<hr /> <hr />
{table.table_type === 'PARTITIONED TABLE' && (
<PartitionInfo table={table} dispatch={dispatch} />
)}
<RootFields tableSchema={table} /> <RootFields tableSchema={table} />
<hr /> <hr />
{getEnumsSection()} {getEnumsSection()}

View File

@ -69,3 +69,27 @@ hr {
margin-bottom: 15px; margin-bottom: 15px;
// overflow: auto; // overflow: auto;
} }
.paddingSm {
padding: 10px !important;
}
.partitionLabel {
margin-right: 4px;
}
.partitionDef {
padding-left: 15px;
}
.quoteText {
color: #960000;
}
.defText {
font-family: monospace;
}
.paddingTopSm {
padding-top: 10px !important;
}

View File

@ -0,0 +1,92 @@
import React, { useEffect, useState } from 'react';
import { Table, Partition } from '../../../../dataSources/types';
import { Dispatch } from '../../../../types';
import { fetchPartitionDetails } from '../DataActions';
import styles from './ModifyTable.scss';
interface Props {
table: Table;
dispatch: Dispatch;
}
const HighlightedText = ({ value }: { value: string }) => {
let insideQuotes = false;
return (
<span className={styles.defText}>
{value.split('').map(k => {
let res = <span>{k}</span>;
if (k === `'` || k === `"`) {
res = <span className={styles.quoteText}>{k}</span>;
insideQuotes = !insideQuotes;
}
if (insideQuotes) {
res = <span className={styles.quoteText}>{k}</span>;
}
return res;
})}
</span>
);
};
const PartitionInfo: React.FC<Props> = ({ table, dispatch }) => {
const [partitions, setPartitions] = useState<Record<string, Partition[]>>({});
useEffect(() => {
dispatch(fetchPartitionDetails(table)).then((data: Partition[]) => {
const partitionsMap = {} as Record<string, Partition[]>;
const unqiuePKs = data
.map(p => p.partition_key)
.filter((elem, index, self) => {
return index === self.indexOf(elem);
});
unqiuePKs.forEach(t => {
const related = data.filter(x => x.partition_key === t);
partitionsMap[t] = related;
});
setPartitions(partitionsMap);
});
}, []);
return (
<div>
{partitions && Object.keys(partitions).length > 0 && (
<>
<h4 className={styles.subheading_text}>Partitions</h4>
{Object.keys(partitions).map(key => (
<div>
<b>
<i
className={`fa fa-columns ${styles.partitionLabel}`}
aria-hidden="true"
/>
created_at -{' '}
</b>
<i>{key}</i>
{partitions[key].map(p => {
return (
<div
className={`${styles.paddingTopSm} ${styles.partitionDef}`}
>
<b>
<i
className={`fa fa-table ${styles.partitionLabel}`}
aria-hidden="true"
/>{' '}
{p.partition_name} -{' '}
</b>
<HighlightedText value={p.partition_def} />
</div>
);
})}
</div>
))}
<hr />
</>
)}
</div>
);
};
export default PartitionInfo;

View File

@ -214,6 +214,7 @@ export const mergeLoadSchemaDataPostgres = (
>[]; >[];
const primaryKeys = JSON.parse(data[2].result[1]) as Table['primary_key'][]; const primaryKeys = JSON.parse(data[2].result[1]) as Table['primary_key'][];
const uniqueKeys = JSON.parse(data[3].result[1]) as any; const uniqueKeys = JSON.parse(data[3].result[1]) as any;
const checkConstraints = dataSource?.checkConstraintsSql const checkConstraints = dataSource?.checkConstraintsSql
? (JSON.parse(data[4].result[1]) as Table['check_constraints']) ? (JSON.parse(data[4].result[1]) as Table['check_constraints'])
: ([] as Table['check_constraints']); : ([] as Table['check_constraints']);

View File

@ -315,6 +315,7 @@ export interface DataSourcesAPI {
aggregationPermissionsAllowed: boolean; aggregationPermissionsAllowed: boolean;
supportedFeatures?: SupportedFeaturesType; supportedFeatures?: SupportedFeaturesType;
defaultRedirectSchema?: string; defaultRedirectSchema?: string;
getPartitionDetailsSql?: (tableName: string, tableSchema: string) => string;
} }
export let currentDriver: Driver = 'postgres'; export let currentDriver: Driver = 'postgres';

View File

@ -62,7 +62,7 @@ const operators = [
]; ];
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const createSQLRegex = /create\s*(?:|or\s*replace)\s*(view|table|function)\s*(?:\s*if*\s*not\s*exists\s*)?((\"?\w+\"?)\.(\"?\w+\"?)|(\"?\w+\"?))/g; const createSQLRegex = /create\s*(?:|or\s*replace)\s*(?<type>view|table|function)\s*(?:\s*if*\s*not\s*exists\s*)?((?<schema>\"?\w+\"?)\.(?<tableWithSchema>\"?\w+\"?)|(?<table>\"?\w+\"?))\s*(?<partition>partition\s*of)?/gim;
export const displayTableName = (table: Table) => { export const displayTableName = (table: Table) => {
const tableName = table.table_name; const tableName = table.table_name;

View File

@ -72,7 +72,7 @@ const columnDataTypes = {
}; };
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const createSQLRegex = /create\s*(?:|or\s*replace)\s*(view|table|function)\s*(?:\s*if*\s*not\s*exists\s*)?((\"?\w+\"?)\.(\"?\w+\"?)|(\"?\w+\"?))/g; const createSQLRegex = /create\s*(?:|or\s*replace)\s*(?<type>view|table|function)\s*(?:\s*if*\s*not\s*exists\s*)?((?<schema>\"?\w+\"?)\.(?<tableWithSchema>\"?\w+\"?)|(?<table>\"?\w+\"?))\s*(?<partition>partition\s*of)?/gim;
export const displayTableName = (table: Table) => { export const displayTableName = (table: Table) => {
const tableName = table.table_name; const tableName = table.table_name;

View File

@ -426,7 +426,7 @@ export const isColTypeString = (colType: string) =>
const dependencyErrorCode = '2BP01'; // pg dependent error > https://www.postgresql.org/docs/current/errcodes-appendix.html const dependencyErrorCode = '2BP01'; // pg dependent error > https://www.postgresql.org/docs/current/errcodes-appendix.html
const createSQLRegex = /create\s*(?:|or\s*replace)\s*(view|table|function)\s*(?:\s*if*\s*not\s*exists\s*)?((\"?\w+\"?)\.(\"?\w+\"?)|(\"?\w+\"?))/g; // eslint-disable-line const createSQLRegex = /create\s*(?:|or\s*replace)\s*(?<type>view|table|function)\s*(?:\s*if*\s*not\s*exists\s*)?((?<schema>\"?\w+\"?)\.(?<tableWithSchema>\"?\w+\"?)|(?<table>\"?\w+\"?))\s*(?<partition>partition\s*of)?/gim; // eslint-disable-line
const isTimeoutError = (error: { const isTimeoutError = (error: {
code: string; code: string;
@ -574,6 +574,23 @@ export const supportedFeatures: SupportedFeaturesType = {
const defaultRedirectSchema = 'public'; const defaultRedirectSchema = 'public';
const getPartitionDetailsSql = (tableName: string, tableSchema: string) => {
return `SELECT
nmsp_parent.nspname AS parent_schema,
parent.relname AS parent_table,
child.relname AS partition_name,
nmsp_child.nspname AS partition_schema,
pg_catalog.pg_get_expr(child.relpartbound, child.oid) AS partition_def,
pg_catalog.pg_get_partkeydef(parent.oid) AS partition_key
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
JOIN pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace
JOIN pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace
WHERE nmsp_child.nspname = '${tableSchema}'
AND parent.relname = '${tableName}';`;
};
export const postgres: DataSourcesAPI = { export const postgres: DataSourcesAPI = {
isTable, isTable,
isJsonColumn, isJsonColumn,
@ -649,4 +666,5 @@ export const postgres: DataSourcesAPI = {
aggregationPermissionsAllowed: true, aggregationPermissionsAllowed: true,
supportedFeatures, supportedFeatures,
defaultRedirectSchema, defaultRedirectSchema,
getPartitionDetailsSql,
}; };

View File

@ -65,6 +65,21 @@ export const getFetchTablesListQuery = (options: {
SELECT SELECT
COALESCE(Json_agg(Row_to_json(info)), '[]' :: json) AS tables COALESCE(Json_agg(Row_to_json(info)), '[]' :: json) AS tables
FROM ( FROM (
with partitions as (
select array(
SELECT
child.relname AS partition
FROM pg_inherits
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
JOIN pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace
${generateWhereClause(
options,
'child.relname',
'nmsp_child.nspname',
'where'
)}
) as names
)
SELECT SELECT
pgn.nspname as table_schema, pgn.nspname as table_schema,
pgc.relname as table_name, pgc.relname as table_name,
@ -80,7 +95,7 @@ export const getFetchTablesListQuery = (options: {
COALESCE(json_agg(DISTINCT row_to_json(ist) :: jsonb || jsonb_build_object('comment', obj_description(pgt.oid))) filter (WHERE ist.trigger_name IS NOT NULL), '[]' :: json) AS triggers, COALESCE(json_agg(DISTINCT row_to_json(ist) :: jsonb || jsonb_build_object('comment', obj_description(pgt.oid))) filter (WHERE ist.trigger_name IS NOT NULL), '[]' :: json) AS triggers,
row_to_json(isv) AS view_info row_to_json(isv) AS view_info
FROM pg_class as pgc FROM partitions, pg_class as pgc
INNER JOIN pg_namespace as pgn INNER JOIN pg_namespace as pgn
ON pgc.relnamespace = pgn.oid ON pgc.relnamespace = pgn.oid
@ -162,6 +177,7 @@ export const getFetchTablesListQuery = (options: {
WHERE WHERE
pgc.relkind IN ('r', 'v', 'f', 'm', 'p') pgc.relkind IN ('r', 'v', 'f', 'm', 'p')
and NOT (pgc.relname = ANY (partitions.names))
${whereQuery} ${whereQuery}
GROUP BY pgc.oid, pgn.nspname, pgc.relname, table_type, isv.* GROUP BY pgc.oid, pgn.nspname, pgc.relname, table_type, isv.*
) AS info; ) AS info;

View File

@ -161,6 +161,15 @@ export interface Table extends BaseTable {
}[]; }[];
} }
export type Partition = {
parent_schema: string;
partition_schema: string;
partition_name: string;
parent_table: string;
partition_def: string;
partition_key: string;
};
export type ColumnAction = 'add' | 'modify'; export type ColumnAction = 'add' | 'modify';
export interface FrequentlyUsedColumn { export interface FrequentlyUsedColumn {