mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
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:
parent
f015234ef6
commit
512c3008b6
@ -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
|
||||
|
||||
|
@ -1334,6 +1334,10 @@ code {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.fontSizeSm {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.cursorPointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -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,6 +365,10 @@ const fetchDataInit = (source, driver) => (dispatch, getState) => {
|
||||
? dataSource.defaultRedirectSchema
|
||||
: schemaList.sort(Intl.Collator().compare)[0];
|
||||
}
|
||||
if (
|
||||
locationBeforeTransitions &&
|
||||
!locationBeforeTransitions.pathname.includes('tables')
|
||||
)
|
||||
dispatch({ type: UPDATE_CURRENT_SCHEMA, currentSchema: newSchema });
|
||||
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) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
@ -1114,4 +1166,5 @@ export {
|
||||
fetchAdditionalColumnsInfo,
|
||||
SET_FILTER_SCHEMA,
|
||||
SET_FILTER_TABLES,
|
||||
fetchPartitionDetails,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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()}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
@ -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']);
|
||||
|
@ -315,6 +315,7 @@ export interface DataSourcesAPI {
|
||||
aggregationPermissionsAllowed: boolean;
|
||||
supportedFeatures?: SupportedFeaturesType;
|
||||
defaultRedirectSchema?: string;
|
||||
getPartitionDetailsSql?: (tableName: string, tableSchema: string) => string;
|
||||
}
|
||||
|
||||
export let currentDriver: Driver = 'postgres';
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user