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)
- console: filter out partitions from track table list and display partition info
## v2.0.0-alpha.10

View File

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

View File

@ -200,6 +200,7 @@ const loadSchema = configOptions => {
),
],
};
if (dataSource?.checkConstraintsSql) {
body.args.push(
getRunSqlQuery(
@ -356,6 +357,7 @@ const fetchDataInit = (source, driver) => (dispatch, getState) => {
schemaList,
});
let newSchema = '';
const { locationBeforeTransitions } = getState().routing;
if (schemaList.length) {
newSchema =
dataSource.defaultRedirectSchema &&
@ -363,7 +365,11 @@ const fetchDataInit = (source, driver) => (dispatch, getState) => {
? dataSource.defaultRedirectSchema
: schemaList.sort(Intl.Collator().compare)[0];
}
dispatch({ type: UPDATE_CURRENT_SCHEMA, currentSchema: newSchema });
if (
locationBeforeTransitions &&
!locationBeforeTransitions.pathname.includes('tables')
)
dispatch({ type: UPDATE_CURRENT_SCHEMA, currentSchema: newSchema });
return dispatch(updateSchemaInfo()); // TODO
},
error => {
@ -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) => {
// eslint-disable-line no-unused-vars
@ -1114,4 +1166,5 @@ export {
fetchAdditionalColumnsInfo,
SET_FILTER_SCHEMA,
SET_FILTER_TABLES,
fetchPartitionDetails,
};

View File

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

View File

@ -17,41 +17,25 @@ const getDefaultSchema = driver => {
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) => {
const _objects = [];
const regExp = services[driver].createSQLRegex;
const matches = sql.match(new RegExp(regExp, 'gmi'));
if (matches) {
matches.forEach(element => {
const itemMatch = element.match(new RegExp(regExp, 'i'));
if (itemMatch && itemMatch.length === 6) {
const _object = {};
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);
}
for (const result of sql.matchAll(regExp)) {
const { type, schema, table, tableWithSchema, partition } =
result.groups ?? {};
if (!type || !(table || tableWithSchema)) continue;
_objects.push({
type: type.toLowerCase(),
schema: getSQLValue(schema || getDefaultSchema(driver)),
name: getSQLValue(table || tableWithSchema),
isPartition: !!partition,
});
}
return _objects;
};

View File

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

View File

@ -69,3 +69,27 @@ hr {
margin-bottom: 15px;
// 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 uniqueKeys = JSON.parse(data[3].result[1]) as any;
const checkConstraints = dataSource?.checkConstraintsSql
? (JSON.parse(data[4].result[1]) as Table['check_constraints'])
: ([] as Table['check_constraints']);

View File

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

View File

@ -62,7 +62,7 @@ const operators = [
];
// 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) => {
const tableName = table.table_name;

View File

@ -72,7 +72,7 @@ const columnDataTypes = {
};
// 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) => {
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 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: {
code: string;
@ -574,6 +574,23 @@ export const supportedFeatures: SupportedFeaturesType = {
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 = {
isTable,
isJsonColumn,
@ -649,4 +666,5 @@ export const postgres: DataSourcesAPI = {
aggregationPermissionsAllowed: true,
supportedFeatures,
defaultRedirectSchema,
getPartitionDetailsSql,
};

View File

@ -65,6 +65,21 @@ export const getFetchTablesListQuery = (options: {
SELECT
COALESCE(Json_agg(Row_to_json(info)), '[]' :: json) AS tables
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
pgn.nspname as table_schema,
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,
row_to_json(isv) AS view_info
FROM pg_class as pgc
FROM partitions, pg_class as pgc
INNER JOIN pg_namespace as pgn
ON pgc.relnamespace = pgn.oid
@ -162,6 +177,7 @@ export const getFetchTablesListQuery = (options: {
WHERE
pgc.relkind IN ('r', 'v', 'f', 'm', 'p')
and NOT (pgc.relname = ANY (partitions.names))
${whereQuery}
GROUP BY pgc.oid, pgn.nspname, pgc.relname, table_type, isv.*
) 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 interface FrequentlyUsedColumn {