allow renaming tables, columns and relationships (close #79) (#1542)

This commit is contained in:
Rakesh Emmadi 2019-03-01 14:47:22 +05:30 committed by Vamshi Surabhi
parent 00227728cb
commit 6c20ca8a55
53 changed files with 2697 additions and 1107 deletions

View File

@ -40,6 +40,40 @@ export const passMTCheckRoute = () => {
);
};
export const passMTRenameTable = () => {
cy.get(getElementFromAlias('heading-edit-table')).click();
cy.get(getElementFromAlias('heading-edit-table-input'))
.clear()
.type(getTableName(3, testName));
cy.get(getElementFromAlias('heading-edit-table-save')).click();
cy.wait(10000);
validateCT(getTableName(3, testName), 'success');
cy.get(getElementFromAlias('heading-edit-table')).click();
cy.get(getElementFromAlias('heading-edit-table-input'))
.clear()
.type(getTableName(0, testName));
cy.get(getElementFromAlias('heading-edit-table-save')).click();
cy.wait(10000);
validateCT(getTableName(0, testName), 'success');
};
export const passMTRenameColumn = () => {
cy.get(getElementFromAlias('edit-id')).click();
cy.get(getElementFromAlias('edit-col-name'))
.clear()
.type(getColName(3));
cy.get(getElementFromAlias('save-button')).click();
cy.wait(2500);
validateColumn(getTableName(0, testName), [getColName(3)], 'success');
cy.get(getElementFromAlias(`edit-${getColName(3)}`)).click();
cy.get(getElementFromAlias('edit-col-name'))
.clear()
.type('id');
cy.get(getElementFromAlias('save-button')).click();
cy.wait(2500);
validateColumn(getTableName(0, testName), ['id'], 'success');
};
export const passMTMoveToTable = () => {
cy.get(getElementFromAlias(getTableName(0, testName))).click();
cy.url().should(

View File

@ -14,6 +14,8 @@ import {
failMCWithWrongDefaultValue,
passCreateForeignKey,
passRemoveForeignKey,
passMTRenameTable,
passMTRenameColumn,
} from './spec';
import { testMode } from '../../../helpers/common';
@ -36,6 +38,8 @@ export const runModifyTableTests = () => {
it('Creating a table', passMTCreateTable);
it('Moving to the table', passMTMoveToTable);
it('Modify table button opens the correct route', passMTCheckRoute);
it('Pass renaming table', passMTRenameTable);
it('Pass renaming column', passMTRenameColumn);
it('Fails to add column without column name', failMTWithoutColName);
it('Fails without type selected', failMTWithoutColType);
it('Add a column', passMTAddColumn);

View File

@ -9,9 +9,8 @@ import {
const delRel = (table, relname) => {
cy.get(getElementFromAlias(table)).click();
cy.get(getElementFromAlias('table-relationships')).click();
cy.get(getElementFromAlias(`remove-button-${relname}`))
.first()
.click();
cy.get(getElementFromAlias(`relationship-toggle-editor-${relname}`)).click();
cy.get(getElementFromAlias(`relationship-remove-${relname}`)).click();
cy.on('window:alert', str => {
expect(str === 'Are you sure?').to.be.true;
});
@ -176,7 +175,7 @@ export const passRTAddSuggestedRel = () => {
.clear()
.type('author');
cy.get(getElementFromAlias('obj-rel-save-0')).click();
cy.wait(15000);
cy.wait(5000);
validateColumn(
'article_table_rt',
['title', { name: 'author', columns: ['name'] }],
@ -189,7 +188,34 @@ export const passRTAddSuggestedRel = () => {
.clear()
.type('comments');
cy.get(getElementFromAlias('arr-rel-save-0')).click();
cy.wait(15000);
cy.wait(5000);
validateColumn(
'article_table_rt',
['title', { name: 'comments', columns: ['comment'] }],
'success'
);
};
export const passRTRenameRelationship = () => {
cy.get(getElementFromAlias('relationship-toggle-editor-comments')).click();
cy.get(getElementFromAlias('relationship-name-input-comments'))
.clear()
.type('comments_renamed');
cy.get(getElementFromAlias('relationship-save-comments')).click();
cy.wait(5000);
validateColumn(
'article_table_rt',
['title', { name: 'comments_renamed', columns: ['comment'] }],
'success'
);
cy.get(
getElementFromAlias('relationship-toggle-editor-comments_renamed')
).click();
cy.get(getElementFromAlias('relationship-name-input-comments_renamed'))
.clear()
.type('comments');
cy.get(getElementFromAlias('relationship-save-comments_renamed')).click();
cy.wait(5000);
validateColumn(
'article_table_rt',
['title', { name: 'comments', columns: ['comment'] }],

View File

@ -12,6 +12,7 @@ import {
passRTAddSuggestedRel,
failRTAddSuggestedRel,
checkAddManualRelationshipsButton,
passRTRenameRelationship,
} from './spec';
import { testMode } from '../../../helpers/common';
import { setMetaData } from '../../validators/validators';
@ -42,6 +43,7 @@ export const runRelationshipsTests = () => {
it('Deleting the relationships', passRTDeleteRelationships);
it('Adding Suggested Relationships Error', failRTAddSuggestedRel);
it('Adding Suggested Relationships', passRTAddSuggestedRel);
it('Rename relationships', passRTRenameRelationship);
it('Deleting the relationships', passRTDeleteRelationships);
it('Deleting testing tables', passRTDeleteTables);
});

View File

@ -397,10 +397,8 @@ export const passVAddManualObjRel = () => {
export const passVDeleteRelationships = () => {
cy.get(getElementFromAlias('author_average_rating_vt')).click();
cy.get(getElementFromAlias('table-relationships')).click();
cy.get('button')
.contains('Remove')
.first()
.click();
cy.get(getElementFromAlias('relationship-toggle-editor-author')).click();
cy.get(getElementFromAlias('relationship-remove-author')).click();
cy.on('window:alert', str => {
expect(str === 'Are you sure?').to.be.true;
});

View File

@ -736,7 +736,7 @@ code
.yellow_button
{
background-color: #FEC53D;
border-radius: 5px;
border-radius: 3px;
color: #606060;
border: 1px solid #FEC53D;
padding: 5px 10px;
@ -861,7 +861,51 @@ code
{
font-size: 18px;
font-weight: bold;
padding-bottom: 20px
}
.editable_heading_text
{
font-size: 18px;
font-weight: bold;
display: flex;
align-items: center;
padding-bottom: 20px;
h2 {
font-size: 18px;
font-weight: bold;
margin-right: 10px;
}
i {
cursor: pointer;
}
}
.editable_heading_textbox
{
display: flex;
align-items: center;
padding-bottom: 12px;
input {
width: 30%;
font-weight: normal;
}
}
.editable_heading_action
{
margin-left: 10px;
display: flex;
align-items: center;
i {
margin-top: 2px;
}
}
.editable_heading_action_item
{
margin-right: 10px;
font-size: 14px;
text-decoration: underline;
font-weight: normal;
cursor: pointer;
}
.header_project_name
{

View File

@ -0,0 +1,92 @@
import React from 'react';
import styles from '../Common.scss';
class Heading extends React.Component {
state = {
text: this.props.currentValue,
isEditting: false,
};
handleTextChange = e => {
this.setState({ text: e.target.value });
};
toggleEditting = () => {
this.setState({ isEditting: !this.state.isEditting });
};
handleKeyPress = e => {
if (this.state.isEditting) {
if (e.charCode === 13) {
this.save();
}
}
};
save = () => {
if (this.props.loading) {
return;
}
this.props.save(this.state.text);
};
render = () => {
const { editable, currentValue, save, loading, property } = this.props;
const { text, isEditting } = this.state;
if (!editable) {
return <h2 className={styles.heading_text}>{currentValue}</h2>;
}
if (!save) {
console.warn('In EditableHeading, please provide a prop save');
}
if (!isEditting) {
return (
<div className={styles.editable_heading_text}>
<h2>{currentValue}</h2>
<div
onClick={this.toggleEditting}
className={styles.editable_heading_action}
data-test={`heading-edit-${property}`}
>
<i className="fa fa-edit" />
</div>
</div>
);
}
return (
<div className={styles.editable_heading_textbox}>
<input
onChange={this.handleTextChange}
className={`${styles.add_pad_left} form-control`}
type="text"
onKeyPress={this.handleKeyPress}
value={text}
data-test={`heading-edit-${property}-input`}
/>
<div className={styles.editable_heading_action}>
<div
className={styles.editable_heading_action_item}
onClick={this.save}
data-test={`heading-edit-${property}-save`}
>
{loading ? 'Saving...' : 'Save'}
</div>
<div
className={styles.editable_heading_action_item}
onClick={this.toggleEditting}
data-test={`heading-edit-${property}-cancel`}
>
Cancel
</div>
</div>
</div>
);
};
}
export default Heading;

View File

@ -8,6 +8,7 @@ const gqlTableErrorNotif = [
custom:
'Table name cannot contain special characters. It can have alphabets, numbers (cannot start with numbers) and _ (can start with _)',
},
'Error renaming table!',
];
const gqlColumnErrorNotif = [
@ -18,6 +19,18 @@ const gqlColumnErrorNotif = [
custom:
'Column name cannot contain special characters. It can have alphabets, numbers (cannot start with numbers) and _ (can start with _)',
},
'Error renaming column!',
];
const gqlViewErrorNotif = [
'Error creating view!',
'View name cannot contain special characters',
'',
{
custom:
'View name cannot contain special characters. It can have alphabets, numbers (cannot start with numbers) and _ (can start with _)',
},
'Error renaming view!',
];
const gqlRelErrorNotif = [
@ -28,7 +41,13 @@ const gqlRelErrorNotif = [
custom:
'Relationship name cannot contain special characters. It can have alphabets, numbers (cannot start with numbers) and _ (can start with _)',
},
'Error renaming relationship!',
];
export default gqlPattern;
export { gqlTableErrorNotif, gqlColumnErrorNotif, gqlRelErrorNotif };
export {
gqlTableErrorNotif,
gqlViewErrorNotif,
gqlColumnErrorNotif,
gqlRelErrorNotif,
};

View File

@ -482,7 +482,8 @@ const makeMigrationCall = (
customOnError,
requestMsg,
successMsg,
errorMsg
errorMsg,
shouldSkipSchemaReload
) => {
const upQuery = {
type: 'bulk',
@ -519,14 +520,16 @@ const makeMigrationCall = (
};
const onSuccess = () => {
if (globals.consoleMode === 'cli') {
dispatch(loadMigrationStatus()); // don't call for server mode
if (!shouldSkipSchemaReload) {
if (globals.consoleMode === 'cli') {
dispatch(loadMigrationStatus()); // don't call for server mode
}
dispatch(loadSchema());
}
dispatch(loadSchema());
customOnSuccess();
if (successMsg) {
dispatch(showSuccessNotification(successMsg));
}
customOnSuccess();
};
const onError = err => {

View File

@ -1,23 +1,44 @@
import React from 'react';
import { Link } from 'react-router';
import Helmet from 'react-helmet';
import globals from '../../../../Globals';
import { changeTableOrViewName } from '../TableModify/ModifyActions';
import EditableHeading from '../../../Common/EditableHeading/EditableHeading';
import { tabNameMap } from '../utils';
const ViewHeader = ({ tableName, tabName, currentSchema, migrationMode }) => {
const ViewHeader = ({
tableName,
tabName,
currentSchema,
migrationMode,
dispatch,
allowRename,
}) => {
const styles = require('../TableCommon/Table.scss');
let capitalised = tabName;
capitalised = capitalised[0].toUpperCase() + capitalised.slice(1);
let activeTab;
if (tabName === 'view') {
activeTab = 'Browse Rows';
} else if (tabName === 'insert') {
activeTab = 'Insert Row';
} else if (tabName === 'modify') {
activeTab = 'Modify';
} else if (tabName === 'relationships') {
activeTab = 'Relationships';
} else if (tabName === 'permissions') {
activeTab = 'Permissions';
}
const activeTab = tabNameMap[tabName];
const viewRenameCallback = newName => {
const currentPath = window.location.pathname.replace(
new RegExp(globals.urlPrefix, 'g'),
''
);
const newPath = currentPath.replace(
/(\/schema\/.*)\/views\/(\w*)(\/.*)?/,
`$1/views/${newName}$3`
);
window.location.replace(
`${window.location.origin}${globals.urlPrefix}${newPath}`
);
};
const saveViewNameChange = newName => {
dispatch(
changeTableOrViewName(false, tableName, newName, () =>
viewRenameCallback(newName)
)
);
};
return (
<div>
<Helmet title={capitalised + ' - ' + tableName + ' - Data | Hasura'} />
@ -40,7 +61,14 @@ const ViewHeader = ({ tableName, tabName, currentSchema, migrationMode }) => {
</Link>{' '}
<i className="fa fa-angle-right" aria-hidden="true" /> {activeTab}
</div>
<h2 className={styles.heading_text}>{tableName}</h2>
<EditableHeading
currentValue={tableName}
save={saveViewNameChange}
loading={false}
editable={tabName === 'modify' && allowRename}
dispatch={dispatch}
property="view"
/>
<div className={styles.nav}>
<ul className="nav nav-pills">
<li

View File

@ -1,6 +1,10 @@
import React from 'react';
import globals from '../../../../Globals';
import { Link } from 'react-router';
import Helmet from 'react-helmet';
import { changeTableOrViewName } from '../TableModify/ModifyActions';
import EditableHeading from '../../../Common/EditableHeading/EditableHeading';
import { tabNameMap } from '../utils';
const TableHeader = ({
tableName,
@ -8,6 +12,8 @@ const TableHeader = ({
count,
migrationMode,
currentSchema,
dispatch,
allowRename,
}) => {
const styles = require('./Table.scss');
let capitalised = tabName;
@ -16,18 +22,30 @@ const TableHeader = ({
if (!(count === null || count === undefined)) {
showCount = '(' + count + ')';
}
let activeTab;
if (tabName === 'view') {
activeTab = 'Browse Rows';
} else if (tabName === 'insert') {
activeTab = 'Insert Row';
} else if (tabName === 'modify') {
activeTab = 'Modify';
} else if (tabName === 'relationships') {
activeTab = 'Relationships';
} else if (tabName === 'permissions') {
activeTab = 'Permissions';
}
const activeTab = tabNameMap[tabName];
const tableRenameCallback = newName => {
const currentPath = window.location.pathname.replace(
new RegExp(globals.urlPrefix, 'g'),
''
);
const newPath = currentPath.replace(
/(\/schema\/.*)\/tables\/(\w*)(\/.*)?/,
`$1/tables/${newName}$3`
);
window.location.replace(
`${window.location.origin}${globals.urlPrefix}${newPath}`
);
};
const saveTableNameChange = newName => {
dispatch(
changeTableOrViewName(true, tableName, newName, () =>
tableRenameCallback(newName)
)
);
};
return (
<div>
<Helmet title={capitalised + ' - ' + tableName + ' - Data | Hasura'} />
@ -52,7 +70,14 @@ const TableHeader = ({
</Link>{' '}
<i className="fa fa-angle-right" aria-hidden="true" /> {activeTab}
</div>
<h2 className={styles.heading_text}>{tableName}</h2>
<EditableHeading
currentValue={tableName}
save={saveTableNameChange}
loading={false}
editable={tabName === 'modify' && allowRename}
dispatch={dispatch}
property="table"
/>
<div className={styles.nav}>
<ul className="nav nav-pills">
<li

View File

@ -50,9 +50,6 @@
min-width: 100px;
font-weight: 300;
}
tr {
cursor: pointer;
}
td {
width: 300px;
max-width: 300px;
@ -124,3 +121,8 @@ a.expanded {
.relationshipTopPadding {
padding: 10px 0;
}
.relEditButtons {
display: flex;
flex-direction:row;
}

View File

@ -0,0 +1,405 @@
import React from 'react';
import { Link } from 'react-router';
import {
fkRefTableChange,
fkRColChange,
toggleFKCheckBox,
isColumnUnique,
deleteConstraintSql,
} from '../TableModify/ModifyActions';
import dataTypes from '../Common/DataTypes';
import { convertListToDictUsingKV } from '../../../../utils/data';
import {
INTEGER,
SERIAL,
BIGINT,
BIGSERIAL,
UUID,
JSONDTYPE,
JSONB,
TIMESTAMP,
TIME,
} from '../../../../constants';
import Button from '../../../Common/Button/Button';
const appPrefix = '/data';
const ColumnEditor = ({
column,
onSubmit,
onDelete,
allSchemas,
fkAdd,
tableName,
dispatch,
currentSchema,
columnComment,
allowRename,
}) => {
// eslint-disable-line no-unused-vars
const c = column;
const styles = require('./Modify.scss');
let [iname, inullable, iunique, idefault, icomment, itype] = [
null,
null,
null,
null,
null,
null,
];
// NOTE: the datatypes is filtered of serial and bigserial where hasuraDatatype === null
const refTable = fkAdd.refTable;
const tableSchema = allSchemas.find(t => t.table_name === tableName);
const rcol = fkAdd.rcol;
const typeMap = convertListToDictUsingKV(
'hasuraDatatype',
'value',
dataTypes.filter(dataType => dataType.hasuraDatatype)
);
const refSchema = allSchemas.find(t => t.table_name === refTable);
// const allTableNamesExceptCurrent = allSchemas.filter(t => t.table_name !== tableName);
const allTableNames = allSchemas.map(t => t.table_name);
allTableNames.sort();
const refColumnNames = refSchema
? refSchema.columns.map(col => col.column_name)
: [];
refColumnNames.sort();
const onFKRefTableChange = e => {
dispatch(fkRefTableChange(e.target.value));
};
const onFKRefColumnChange = e => {
dispatch(fkRColChange(e.target.value));
};
const checkExistingForeignKey = () => {
const numFk = tableSchema.foreign_key_constraints.length;
let fkName = '';
const onDeleteFK = e => {
e.preventDefault();
const isOk = confirm('Are you sure?');
if (isOk) {
dispatch(deleteConstraintSql(tableName, fkName));
}
};
if (numFk > 0) {
for (let i = 0; i < numFk; i++) {
const fk = tableSchema.foreign_key_constraints[i];
if (
Object.keys(fk.column_mapping).toString() === c.column_name.toString()
) {
fkName = fk.constraint_name;
return (
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Foreign Key</label>
<div className="col-xs-9">
<h5>
<span>{fk.ref_table} :: </span>
<span className={styles.add_mar_right}>
{Object.keys(fk.column_mapping)
.map(l => fk.column_mapping[l])
.join(',')}
</span>
<Link
to={`${appPrefix}/schema/${currentSchema}/tables/${tableName}/relationships`}
>
<Button
color="white"
size="sm"
type="button"
data-test="add-rel-mod"
>
+Add relationship
</Button>
</Link>
&nbsp;
<Button
color="red"
size="sm"
onClick={onDeleteFK}
data-test="remove-constraint-button"
>
{' '}
Remove Constraint{' '}
</Button>{' '}
&nbsp;
</h5>
</div>
</div>
);
}
}
}
return (
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">
<input
type="checkbox"
checked={fkAdd.fkCheckBox}
onChange={e => {
dispatch(toggleFKCheckBox(e.target.checked));
}}
value="ForeignKey"
data-test="foreign-key-checkbox"
/>{' '}
Foreign Key
</label>
<div className="col-xs-6">
<select
className={`${styles.fkSelect} ${styles.fkInEdit} ${
styles.fkInEditLeft
} input-sm form-control`}
disabled={fkAdd.fkCheckBox === false}
value={refTable}
onChange={onFKRefTableChange}
data-test="ref-table"
>
<option disabled value="">
Reference table
</option>
{allTableNames.map((tName, i) => (
<option key={i} value={tName}>
{tName}
</option>
))}
</select>
<select
className={`${styles.fkSelect} ${
styles.fkInEdit
} input-sm form-control`}
disabled={fkAdd.fkCheckBox === false}
value={rcol}
onChange={onFKRefColumnChange}
data-test="ref-col"
>
<option disabled value="">
Reference column
</option>
{refColumnNames.map((co, i) => (
<option key={i} value={co}>
{co}
</option>
))}
</select>
</div>
</div>
);
};
let isPrimaryKey = false;
const isUnique = isColumnUnique(tableSchema, c.column_name);
if (
tableSchema.primary_key &&
tableSchema.primary_key.columns.includes(c.column_name)
) {
isPrimaryKey = true;
}
const additionalOptions = [];
let finalDefaultValue = typeMap[c.data_type];
if (!typeMap[c.data_type]) {
finalDefaultValue = c.data_type;
additionalOptions.push(
<option value={finalDefaultValue} key={finalDefaultValue}>
{c.data_type}
</option>
);
}
const generateAlterOptions = datatypeOptions => {
return dataTypes.map(datatype => {
if (datatypeOptions.includes(datatype.value)) {
return (
<option
value={datatype.value}
key={datatype.name}
title={datatype.description}
>
{datatype.name}
</option>
);
}
});
};
const modifyAlterOptions = columntype => {
const integerOptions = [
'integer',
'serial',
'bigint',
'bigserial',
'numeric',
'text',
];
const bigintOptions = ['bigint', 'bigserial', 'text', 'numeric'];
const uuidOptions = ['uuid', 'text'];
const jsonOptions = ['json', 'jsonb', 'text'];
const timestampOptions = ['timestamptz', 'text'];
const timeOptions = ['timetz', 'text'];
switch (columntype) {
case INTEGER:
return generateAlterOptions(integerOptions);
case SERIAL:
return generateAlterOptions(integerOptions);
case BIGINT:
return generateAlterOptions(bigintOptions);
case BIGSERIAL:
return generateAlterOptions(bigintOptions);
case UUID:
return generateAlterOptions(uuidOptions);
case JSONDTYPE:
return generateAlterOptions(jsonOptions);
case JSONB:
return generateAlterOptions(jsonOptions);
case TIMESTAMP:
return generateAlterOptions(timestampOptions);
case TIME:
return generateAlterOptions(timeOptions);
default:
return generateAlterOptions([columntype, 'text']);
}
};
return (
<div className={`${styles.colEditor} container-fluid`}>
<form
className="form-horizontal"
onSubmit={e => {
e.preventDefault();
onSubmit(
itype.value,
inullable.value,
iunique.value,
idefault.value,
icomment.value,
column,
allowRename ? iname.value : null
);
}}
>
{allowRename && (
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Name</label>
<div className="col-xs-6">
<input
ref={n => (iname = n)}
className="input-sm form-control"
defaultValue={column.column_name}
type="text"
data-test="edit-col-name"
/>
</div>
</div>
)}
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Type</label>
<div className="col-xs-6">
<select
ref={n => (itype = n)}
className="input-sm form-control"
defaultValue={finalDefaultValue}
disabled={isPrimaryKey}
>
{modifyAlterOptions(column.data_type)}
{additionalOptions}
</select>
</div>
</div>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Nullable</label>
<div className="col-xs-6">
<select
ref={n => (inullable = n)}
className="input-sm form-control"
defaultValue={c.is_nullable === 'NO' ? 'false' : 'true'}
disabled={isPrimaryKey}
data-test="edit-col-nullable"
>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
</div>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Unique</label>
<div className="col-xs-6">
<select
ref={n => (iunique = n)}
className="input-sm form-control"
defaultValue={isUnique.toString()}
disabled={isPrimaryKey}
data-test="edit-col-unique"
>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
</div>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Default</label>
<div className="col-xs-6">
<input
ref={n => (idefault = n)}
className="input-sm form-control"
defaultValue={c.column_default ? c.column_default : null}
type="text"
disabled={isPrimaryKey}
data-test="edit-col-default"
/>
</div>
</div>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Comment</label>
<div className="col-xs-6">
<input
ref={n => (icomment = n)}
className="input-sm form-control"
defaultValue={columnComment ? columnComment.result[1] : null}
type="text"
data-test="edit-col-comment"
/>
</div>
</div>
{checkExistingForeignKey()}
<div className="row">
<Button
type="submit"
color="yellow"
className={styles.button_mar_right}
size="sm"
data-test="save-button"
>
Save
</Button>
{!isPrimaryKey ? (
<Button
type="submit"
color="red"
size="sm"
onClick={e => {
e.preventDefault();
onDelete();
}}
data-test="remove-button"
>
Remove
</Button>
) : null}
</div>
</form>
<div className="row">
<br />
</div>
</div>
);
};
export default ColumnEditor;

View File

@ -336,7 +336,6 @@ hr
}
}
}
.chevron_mar_right {
margin-right: 5px;
}

View File

@ -16,6 +16,11 @@ import {
import dataHeaders from '../Common/Headers';
import { UPDATE_MIGRATION_STATUS_ERROR } from '../../../Main/Actions';
import { getAllUnTrackedRelations } from '../TableRelationships/Actions';
import gqlPattern, {
gqlTableErrorNotif,
gqlViewErrorNotif,
gqlColumnErrorNotif,
} from '../Common/GraphQLValidation';
const TOGGLE_ACTIVE_COLUMN = 'ModifyTable/TOGGLE_ACTIVE_COLUMN';
const RESET = 'ModifyTable/RESET';
@ -23,6 +28,8 @@ const RESET = 'ModifyTable/RESET';
const VIEW_DEF_REQUEST_SUCCESS = 'ModifyTable/VIEW_DEF_REQUEST_SUCCESS';
const VIEW_DEF_REQUEST_ERROR = 'ModifyTable/VIEW_DEF_REQUEST_ERROR';
const SAVE_NEW_TABLE_NAME = 'ModifyTable/SAVE_NEW_TABLE_NAME';
const TABLE_COMMENT_EDIT = 'ModifyTable/TABLE_COMMENT_EDIT';
const TABLE_COMMENT_INPUT_EDIT = 'ModifyTable/TABLE_COMMENT_INPUT_EDIT';
const FK_SET_REF_TABLE = 'ModifyTable/FK_SET_REF_TABLE';
@ -33,6 +40,78 @@ const FK_ADD_FORM_ERROR = 'ModifyTable/FK_ADD_FORM_ERROR';
const FK_RESET = 'ModifyTable/FK_RESET';
const TOGGLE_FK_CHECKBOX = 'ModifyTable/TOGGLE_FK_CHECKBOX';
const changeTableOrViewName = (isTable, oldName, newName, callback) => {
return (dispatch, getState) => {
const property = isTable ? 'table' : 'view';
dispatch({ type: SAVE_NEW_TABLE_NAME });
if (oldName === newName) {
return dispatch(
showErrorNotification(
`Renaming ${property} failed`,
`The ${property} name is already ${oldName}`
)
);
}
if (!gqlPattern.test(newName)) {
const gqlValidationError = isTable
? gqlTableErrorNotif
: gqlViewErrorNotif;
return dispatch(
showErrorNotification(
gqlValidationError[4],
gqlValidationError[1],
gqlValidationError[2],
gqlValidationError[3]
)
);
}
const currentSchema = getState().tables.currentSchema;
const migrateUp = [
{
type: 'run_sql',
args: {
sql: `alter ${property} "${currentSchema}"."${oldName}" rename to "${newName}";`,
},
},
];
const migrateDown = [
{
type: 'run_sql',
args: {
sql: `alter ${property} "${currentSchema}"."${newName}" rename to "${oldName}";`,
},
},
];
// apply migrations
const migrationName = `rename_${property}_` + currentSchema + '_' + oldName;
const requestMsg = `Renaming ${property}...`;
const successMsg = `Renaming ${property} successful`;
const errorMsg = `Renaming ${property} failed`;
const customOnSuccess = () => {
callback();
};
const customOnError = err => {
dispatch({ type: UPDATE_MIGRATION_STATUS_ERROR, data: err });
};
makeMigrationCall(
dispatch,
getState,
migrateUp,
migrateDown,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg,
true
);
};
};
// TABLE MODIFY
const deleteTableSql = tableName => {
return (dispatch, getState) => {
@ -727,7 +806,8 @@ const saveColumnChangesSql = (
unique,
def,
comment,
column
column,
newName
) => {
// eslint-disable-line no-unused-vars
return (dispatch, getState) => {
@ -1222,6 +1302,32 @@ const saveColumnChangesSql = (
});
}
/* rename column */
if (newName && colName !== newName) {
if (!gqlPattern.test(newName)) {
return dispatch(
showErrorNotification(
gqlColumnErrorNotif[4],
gqlColumnErrorNotif[1],
gqlColumnErrorNotif[2],
gqlColumnErrorNotif[3]
)
);
}
schemaChangesUp.push({
type: 'run_sql',
args: {
sql: `alter table "${currentSchema}"."${tableName}" rename column "${colName}" to "${newName}";`,
},
});
schemaChangesDown.push({
type: 'run_sql',
args: {
sql: `alter table "${currentSchema}"."${tableName}" rename column "${newName}" to "${colName}";`,
},
});
}
// Apply migrations
const migrationName =
'alter_table_' +
@ -1269,7 +1375,8 @@ const saveColChangesWithFkSql = (
unique,
def,
comment,
column
column,
newName
) => {
// ALTER TABLE <table> ALTER COLUMN <column> TYPE <column_type>;
const colType = type;
@ -1772,6 +1879,22 @@ const saveColChangesWithFkSql = (
}
}
/* rename column */
if (newName && newName !== colName) {
schemaChangesUp.push({
type: 'run_sql',
args: {
sql: `alter table "${currentSchema}"."${tableName}" rename column "${colName}" to "${newName}";`,
},
});
schemaChangesDown.push({
type: 'run_sql',
args: {
sql: `alter table "${currentSchema}"."${tableName}" rename column "${newName}" to "${colName}";`,
},
});
}
// Apply migrations
const migrationName =
'alter_table_' +
@ -1825,6 +1948,8 @@ export {
TOGGLE_FK_CHECKBOX,
TABLE_COMMENT_EDIT,
TABLE_COMMENT_INPUT_EDIT,
SAVE_NEW_TABLE_NAME,
changeTableOrViewName,
fetchViewDefinition,
handleMigrationErrors,
saveColumnChangesSql,

View File

@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Link } from 'react-router';
import React from 'react';
import TableHeader from '../TableCommon/TableHeader';
import {
activateCommentEdit,
@ -8,27 +7,19 @@ import {
saveTableCommentSql,
} from './ModifyActions';
import {
fkRefTableChange,
fkLColChange,
fkRColChange,
toggleFKCheckBox,
saveColChangesWithFkSql,
isColumnUnique,
deleteTableSql,
deleteConstraintSql,
addColSql,
untrackTableSql,
RESET,
TOGGLE_ACTIVE_COLUMN,
saveColumnChangesSql,
saveColChangesWithFkSql,
deleteColumnSql,
} from '../TableModify/ModifyActions';
import { ordinalColSort } from '../utils';
import dataTypes from '../Common/DataTypes';
import {
convertListToDictUsingKV,
convertListToDict,
} from '../../../../utils/data';
import { convertListToDict } from '../../../../utils/data';
import {
setTable,
fetchTableComment,
@ -36,20 +27,9 @@ import {
} from '../DataActions';
import { showErrorNotification } from '../Notification';
import gqlPattern, { gqlColumnErrorNotif } from '../Common/GraphQLValidation';
import {
INTEGER,
SERIAL,
BIGINT,
BIGSERIAL,
UUID,
JSONDTYPE,
JSONB,
TIMESTAMP,
TIME,
} from '../../../../constants';
import Button from '../../../Common/Button/Button';
const appPrefix = '/data';
import ColumnEditor from './ColumnEditor';
import semverCheck from '../../../../helpers/semver';
const alterTypeOptions = dataTypes.map((datatype, index) => (
<option value={datatype.value} key={index} title={datatype.description}>
@ -57,375 +37,42 @@ const alterTypeOptions = dataTypes.map((datatype, index) => (
</option>
));
const ColumnEditor = ({
column,
onSubmit,
onDelete,
allSchemas,
fkAdd,
tableName,
dispatch,
currentSchema,
columnComment,
}) => {
// eslint-disable-line no-unused-vars
const c = column;
const styles = require('./Modify.scss');
let [inullable, iunique, idefault, icomment, itype] = [
null,
null,
null,
null,
null,
];
// NOTE: the datatypes is filtered of serial and bigserial where hasuraDatatype === null
const refTable = fkAdd.refTable;
const tableSchema = allSchemas.find(t => t.table_name === tableName);
const rcol = fkAdd.rcol;
const typeMap = convertListToDictUsingKV(
'hasuraDatatype',
'value',
dataTypes.filter(dataType => dataType.hasuraDatatype)
);
const refSchema = allSchemas.find(t => t.table_name === refTable);
// const allTableNamesExceptCurrent = allSchemas.filter(t => t.table_name !== tableName);
const allTableNames = allSchemas.map(t => t.table_name);
allTableNames.sort();
const refColumnNames = refSchema
? refSchema.columns.map(col => col.column_name)
: [];
refColumnNames.sort();
const onFKRefTableChange = e => {
dispatch(fkRefTableChange(e.target.value));
};
const onFKRefColumnChange = e => {
dispatch(fkRColChange(e.target.value));
};
const checkExistingForeignKey = () => {
const numFk = tableSchema.foreign_key_constraints.length;
let fkName = '';
const onDeleteFK = e => {
e.preventDefault();
const isOk = confirm('Are you sure?');
if (isOk) {
dispatch(deleteConstraintSql(tableName, fkName));
}
};
if (numFk > 0) {
for (let i = 0; i < numFk; i++) {
const fk = tableSchema.foreign_key_constraints[i];
if (
Object.keys(fk.column_mapping).toString() === c.column_name.toString()
) {
fkName = fk.constraint_name;
return (
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Foreign Key</label>
<div className="col-xs-9">
<h5>
<span>{fk.ref_table} :: </span>
<span className={styles.add_mar_right}>
{Object.keys(fk.column_mapping)
.map(l => fk.column_mapping[l])
.join(',')}
</span>
<Link
to={`${appPrefix}/schema/${currentSchema}/tables/${tableName}/relationships`}
>
<Button
color="white"
size="sm"
type="button"
data-test="add-rel-mod"
>
+Add relationship
</Button>
</Link>
&nbsp;
<Button
color="red"
size="sm"
onClick={onDeleteFK}
data-test="remove-constraint-button"
>
{' '}
Remove Constraint{' '}
</Button>{' '}
&nbsp;
</h5>
</div>
</div>
);
}
}
}
return (
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">
<input
type="checkbox"
checked={fkAdd.fkCheckBox}
onChange={e => {
dispatch(toggleFKCheckBox(e.target.checked));
}}
value="ForeignKey"
data-test="foreign-key-checkbox"
/>{' '}
Foreign Key
</label>
<div className="col-xs-6">
<select
className={`${styles.fkSelect} ${styles.fkInEdit} ${
styles.fkInEditLeft
} input-sm form-control`}
disabled={fkAdd.fkCheckBox === false}
value={refTable}
onChange={onFKRefTableChange}
data-test="ref-table"
>
<option disabled value="">
Reference table
</option>
{allTableNames.map((tName, i) => (
<option key={i} value={tName}>
{tName}
</option>
))}
</select>
<select
className={`${styles.fkSelect} ${
styles.fkInEdit
} input-sm form-control`}
disabled={fkAdd.fkCheckBox === false}
value={rcol}
onChange={onFKRefColumnChange}
data-test="ref-col"
>
<option disabled value="">
Reference column
</option>
{refColumnNames.map((co, i) => (
<option key={i} value={co}>
{co}
</option>
))}
</select>
</div>
</div>
);
};
let isPrimaryKey = false;
const isUnique = isColumnUnique(tableSchema, c.column_name);
if (
tableSchema.primary_key &&
tableSchema.primary_key.columns.includes(c.column_name)
) {
isPrimaryKey = true;
}
const additionalOptions = [];
let finalDefaultValue = typeMap[c.data_type];
if (!typeMap[c.data_type]) {
finalDefaultValue = c.data_type;
additionalOptions.push(
<option value={finalDefaultValue} key={finalDefaultValue}>
{c.data_type}
</option>
);
}
const generateAlterOptions = datatypeOptions => {
return dataTypes.map(datatype => {
if (datatypeOptions.includes(datatype.value)) {
return (
<option
value={datatype.value}
key={datatype.name}
title={datatype.description}
>
{datatype.name}
</option>
);
}
});
class ModifyTable extends React.Component {
state = {
supportTableColumnRename: false,
};
const modifyAlterOptions = columntype => {
const integerOptions = [
'integer',
'serial',
'bigint',
'bigserial',
'numeric',
'text',
];
const bigintOptions = ['bigint', 'bigserial', 'text', 'numeric'];
const uuidOptions = ['uuid', 'text'];
const jsonOptions = ['json', 'jsonb', 'text'];
const timestampOptions = ['timestamptz', 'text'];
const timeOptions = ['timetz', 'text'];
switch (columntype) {
case INTEGER:
return generateAlterOptions(integerOptions);
case SERIAL:
return generateAlterOptions(integerOptions);
case BIGINT:
return generateAlterOptions(bigintOptions);
case BIGSERIAL:
return generateAlterOptions(bigintOptions);
case UUID:
return generateAlterOptions(uuidOptions);
case JSONDTYPE:
return generateAlterOptions(jsonOptions);
case JSONB:
return generateAlterOptions(jsonOptions);
case TIMESTAMP:
return generateAlterOptions(timestampOptions);
case TIME:
return generateAlterOptions(timeOptions);
default:
return generateAlterOptions([columntype, 'text']);
}
};
return (
<div className={`${styles.colEditor} container-fluid`}>
<form
className="form-horizontal"
onSubmit={e => {
e.preventDefault();
onSubmit(
itype.value,
inullable.value,
iunique.value,
idefault.value,
icomment.value,
column
);
}}
>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Type</label>
<div className="col-xs-6">
<select
ref={n => (itype = n)}
className="input-sm form-control"
defaultValue={finalDefaultValue}
disabled={isPrimaryKey}
>
{modifyAlterOptions(column.data_type)}
{additionalOptions}
</select>
</div>
</div>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Nullable</label>
<div className="col-xs-6">
<select
ref={n => (inullable = n)}
className="input-sm form-control"
defaultValue={c.is_nullable === 'NO' ? 'false' : 'true'}
disabled={isPrimaryKey}
data-test="edit-col-nullable"
>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
</div>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Unique</label>
<div className="col-xs-6">
<select
ref={n => (iunique = n)}
className="input-sm form-control"
defaultValue={isUnique.toString()}
disabled={isPrimaryKey}
data-test="edit-col-unique"
>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
</div>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Default</label>
<div className="col-xs-6">
<input
ref={n => (idefault = n)}
className="input-sm form-control"
defaultValue={c.column_default ? c.column_default : null}
type="text"
disabled={isPrimaryKey}
data-test="edit-col-default"
/>
</div>
</div>
<div className={`${styles.display_flex} form-group`}>
<label className="col-xs-3 text-right">Comment</label>
<div className="col-xs-6">
<input
ref={n => (icomment = n)}
className="input-sm form-control"
defaultValue={columnComment ? columnComment.result[1] : null}
type="text"
data-test="edit-col-comment"
/>
</div>
</div>
{checkExistingForeignKey()}
<div className="row">
<Button
type="submit"
color="yellow"
className={styles.button_mar_right}
size="sm"
data-test="save-button"
>
Save
</Button>
{!isPrimaryKey ? (
<Button
type="submit"
color="red"
size="sm"
onClick={e => {
e.preventDefault();
onDelete();
}}
data-test="remove-button"
>
Remove
</Button>
) : null}
</div>
</form>
<div className="row">
<br />
</div>
</div>
);
};
class ModifyTable extends Component {
componentDidMount() {
const { dispatch } = this.props;
const { dispatch, serverVersion } = this.props;
dispatch({ type: RESET });
dispatch(setTable(this.props.tableName));
dispatch(fetchTableComment(this.props.tableName));
if (serverVersion) {
this.checkTableColumnRenameSupport(serverVersion);
}
}
componentWillReceiveProps(nextProps) {
if (
nextProps.serverVersion &&
nextProps.serverVersion !== this.props.serverVersion
) {
this.checkTableColumnRenameSupport(nextProps.serverVersion);
}
}
checkTableColumnRenameSupport = serverVersion => {
try {
if (semverCheck('tableColumnRename', serverVersion)) {
this.setState({
supportTableColumnRename: true,
});
}
} catch (e) {
console.error(e);
}
};
render() {
const {
tableName,
@ -455,7 +102,15 @@ class ModifyTable extends Component {
let colEditor = null;
let bg = '';
const colName = c.column_name;
const onSubmit = (type, nullable, unique, def, comment, column) => {
const onSubmit = (
type,
nullable,
unique,
def,
comment,
column,
newName
) => {
// dispatch(saveColumnChangesSql(tableName, colName, type, nullable, def, column));
if (fkAdd.fkCheckBox === true) {
dispatch(fkLColChange(column.column_name));
@ -468,7 +123,8 @@ class ModifyTable extends Component {
unique,
def,
comment,
column
column,
newName
)
);
} else {
@ -481,7 +137,8 @@ class ModifyTable extends Component {
unique,
def,
comment,
column
column,
newName
)
);
}
@ -512,6 +169,7 @@ class ModifyTable extends Component {
allSchemas={allSchemas}
currentSchema={currentSchema}
columnComment={columnComment}
allowRename={this.state.supportTableColumnRename}
/>
);
} else {
@ -526,6 +184,7 @@ class ModifyTable extends Component {
allSchemas={allSchemas}
currentSchema={currentSchema}
columnComment={columnComment}
allowRename={this.state.supportTableColumnRename}
/>
);
}
@ -717,6 +376,7 @@ class ModifyTable extends Component {
tabName="modify"
migrationMode={migrationMode}
currentSchema={currentSchema}
allowRename={this.state.supportTableColumnRename}
/>
<br />
<div className={`container-fluid ${styles.padd_left_remove}`}>
@ -872,12 +532,14 @@ ModifyTable.propTypes = {
lastFormError: PropTypes.object,
lastSuccess: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
serverVersion: PropTypes.string,
};
const mapStateToProps = (state, ownProps) => ({
tableName: ownProps.params.table,
allSchemas: state.tables.allSchemas,
migrationMode: state.main.migrationMode,
serverVersion: state.main.serverVersion,
currentSchema: state.tables.currentSchema,
tableComment: state.tables.tableComment,
columnComment: state.tables.columnComment,

View File

@ -16,16 +16,44 @@ import {
import { ordinalColSort } from '../utils';
import { setTable, fetchTableComment } from '../DataActions';
import Button from '../../../Common/Button/Button';
import semverCheck from '../../../../helpers/semver';
class ModifyView extends Component {
state = {
supportTableColumnRename: false,
};
componentDidMount() {
const { dispatch } = this.props;
const { dispatch, serverVersion } = this.props;
dispatch({ type: RESET });
dispatch(setTable(this.props.tableName));
dispatch(fetchViewDefinition(this.props.tableName, false));
dispatch(fetchTableComment(this.props.tableName));
if (serverVersion) {
this.checkTableColumnRenameSupport(serverVersion);
}
}
componentWillReceiveProps(nextProps) {
if (
nextProps.serverVersion &&
nextProps.serverVersion !== this.props.serverVersion
) {
this.checkTableColumnRenameSupport(nextProps.serverVersion);
}
}
checkTableColumnRenameSupport = serverVersion => {
try {
if (semverCheck('tableColumnRename', serverVersion)) {
this.setState({
supportTableColumnRename: true,
});
}
} catch (e) {
console.error(e);
}
};
modifyViewDefinition = viewName => {
// fetch the definition
this.props.dispatch(fetchViewDefinition(viewName, true));
@ -191,6 +219,7 @@ class ModifyView extends Component {
tabName="modify"
currentSchema={currentSchema}
migrationMode={migrationMode}
allowRename={this.state.supportTableColumnRename}
/>
<br />
<div className={'container-fluid ' + styles.padd_left_remove}>
@ -261,6 +290,7 @@ ModifyView.propTypes = {
lastError: PropTypes.object,
lastSuccess: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
serverVersion: PropTypes.string,
};
const mapStateToProps = (state, ownProps) => {
@ -271,6 +301,7 @@ const mapStateToProps = (state, ownProps) => {
currentSchema: state.tables.currentSchema,
tableComment: state.tables.tableComment,
migrationMode: state.main.migrationMode,
serverVersion: state.main.serverVersion,
...state.tables.modify,
};
};

View File

@ -36,6 +36,56 @@ const relTypeChange = isObjRel => ({
});
const relRTableChange = rTable => ({ type: REL_SET_RTABLE, rTable });
const saveRenameRelationship = (oldName, newName, tableName, callback) => {
return (dispatch, getState) => {
const currentSchema = getState().tables.currentSchema;
const migrateUp = [
{
type: 'rename_relationship',
args: {
table: tableName,
name: oldName,
new_name: newName,
},
},
];
const migrateDown = [
{
type: 'rename_relationship',
args: {
table: tableName,
name: newName,
new_name: oldName,
},
},
];
// Apply migrations
const migrationName = `rename_relationship_${oldName}_to_${newName}_schema_${currentSchema}_table_${tableName}`;
const requestMsg = 'Renaming relationship...';
const successMsg = 'Relationship renamed';
const errorMsg = 'Renaming relationship failed';
const customOnSuccess = () => {
callback();
};
const customOnError = () => {};
makeMigrationCall(
dispatch,
getState,
migrateUp,
migrateDown,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
);
};
};
const generateRelationshipsQuery = (
tableName,
relName,
@ -487,4 +537,5 @@ export {
autoAddRelName,
formRelName,
getAllUnTrackedRelations,
saveRenameRelationship,
};

View File

@ -0,0 +1,154 @@
/* eslint-disable jsx-a11y/no-autofocus */
import React from 'react';
import { getRelationshipLine } from './utils';
import Button from '../../../Common/Button/Button';
import { deleteRelMigrate, saveRenameRelationship } from './Actions';
import { showErrorNotification } from '../Notification';
import gqlPattern, { gqlRelErrorNotif } from '../Common/GraphQLValidation';
import styles from '../TableModify/Modify.scss';
class RelationshipEditor extends React.Component {
state = {
isEditting: false,
text: this.props.relName,
};
handleTextChange = e => {
this.setState({
text: e.target.value,
});
};
toggleEditor = () => {
this.setState({
isEditting: !this.state.isEditting,
});
};
handleKeyPress = e => {
if (this.state.isEditting) {
if (e.charCode === 13) {
this.save();
}
}
};
save = () => {
const { tableName, relName, dispatch } = this.props;
const { text } = this.state;
if (text === relName) {
return dispatch(
showErrorNotification(
'Renaming relationship failed',
`The relationship name is already ${relName}`
)
);
}
if (!gqlPattern.test(text)) {
return dispatch(
showErrorNotification(
gqlRelErrorNotif[4],
gqlRelErrorNotif[1],
gqlRelErrorNotif[2],
gqlRelErrorNotif[3]
)
);
}
dispatch(
saveRenameRelationship(relName, text, tableName, this.toggleEditor)
);
};
render() {
const {
dispatch,
tableName,
relName,
relConfig,
isObjRel,
tableStyles,
allowRename,
} = this.props;
const { text, isEditting } = this.state;
const { lcol, rtable, rcol } = relConfig;
const onDelete = e => {
e.preventDefault();
const isOk = confirm('Are you sure?');
if (isOk) {
dispatch(
deleteRelMigrate(tableName, relName, lcol, rtable, rcol, isObjRel)
);
}
};
const collapsed = () => (
<div>
<Button
color={allowRename ? 'white' : 'red'}
size={allowRename ? 'xs' : 'sm'}
onClick={allowRename ? this.toggleEditor : onDelete}
data-test={
allowRename
? `relationship-toggle-editor-${relName}`
: `relationship-remove-${relName}`
}
>
{allowRename ? 'Edit' : 'Remove'}
</Button>
&nbsp;
<b>{relName}</b>
<div className={tableStyles.relationshipTopPadding}>
{getRelationshipLine(isObjRel, lcol, rcol, rtable)}
</div>
</div>
);
const expanded = () => (
<div className={styles.activeEdit}>
<div className={tableStyles.add_mar_top}>
<Button
color="white"
size="xs"
onClick={this.toggleEditor}
data-test={`relationship-toggle-editor-${relName}`}
>
Close
</Button>
</div>
<div className={tableStyles.relationshipTopPadding}>
<div>{getRelationshipLine(isObjRel, lcol, rcol, rtable)}</div>
<input
onChange={this.handleTextChange}
className={`form-control ${styles.add_mar_top_small}`}
type="text"
value={text}
data-test={`relationship-name-input-${relName}`}
onKeyPress={this.handleKeyPress}
autoFocus
/>
</div>
<div className={tableStyles.relEditButtons}>
<Button
className={styles.add_mar_right}
color="yellow"
size="xs"
onClick={this.save}
data-test={`relationship-save-${relName}`}
>
Save
</Button>
<Button
color="red"
size="xs"
onClick={onDelete}
data-test={`relationship-remove-${relName}`}
>
Remove
</Button>
</div>
</div>
);
return <td>{isEditting ? expanded() : collapsed()}</td>;
}
}
export default RelationshipEditor;

View File

@ -3,7 +3,6 @@ import React, { Component } from 'react';
import TableHeader from '../TableCommon/TableHeader';
import { RESET } from '../TableModify/ModifyActions';
import {
deleteRelMigrate,
addNewRelClicked,
addRelNewFromStateMigrate,
relSelectionChanged,
@ -16,10 +15,13 @@ import { findAllFromRel } from '../utils';
import { showErrorNotification } from '../Notification';
import { setTable } from '../DataActions';
import gqlPattern, { gqlRelErrorNotif } from '../Common/GraphQLValidation';
import { getRelationshipLine } from './utils';
import AddManualRelationship from './AddManualRelationship';
import suggestedRelationshipsRaw from './autoRelations';
import RelationshipEditor from './RelationshipEditor';
import Button from '../../../Common/Button/Button';
import semverCheck from '../../../../helpers/semver';
/* Gets the complete list of relationships and converts it to a list of object, which looks like so :
{
@ -43,64 +45,6 @@ const getObjArrayRelationshipList = relationships => {
return requiredList;
};
/* This function sets the styling to the way the relationship looks, for eg: id -> user::user_id */
const getRelationshipLine = (isObjRel, lcol, rcol, rTable) => {
const finalRTable = rTable.name ? rTable.name : rTable;
return isObjRel ? (
<span>
&nbsp;
{lcol.join(',')}
&nbsp;&nbsp;&rarr;&nbsp;&nbsp;
{rTable} :: {rcol.join(',')}
</span>
) : (
<span>
&nbsp;
{finalRTable} :: {rcol.join(',')}
&nbsp;&nbsp;&rarr;&nbsp;&nbsp;
{lcol.join(',')}
</span>
);
};
const relationshipView = (
dispatch,
tableName,
relName,
{ lcol, rtable, rcol },
isObjRel,
tableStyles
) => {
const onDelete = e => {
e.preventDefault();
const isOk = confirm('Are you sure?');
if (isOk) {
dispatch(
deleteRelMigrate(tableName, relName, lcol, rtable, rcol, isObjRel)
);
}
};
return (
<td>
<div>
<Button
color="red"
size="sm"
onClick={onDelete}
data-test={`remove-button-${relName}`}
>
Remove
</Button>
&nbsp;
<b>{relName}</b>
<div className={tableStyles.relationshipTopPadding}>
{getRelationshipLine(isObjRel, lcol, rcol, rtable)}
</div>
</div>
</td>
);
};
const addRelationshipCellView = (
dispatch,
rel,
@ -179,7 +123,7 @@ const addRelationshipCellView = (
<Button
type="submit"
color="yellow"
size="sm"
size="xs"
data-test={
relMetaData[0] === 'object'
? `obj-rel-save-${relMetaData[1]}`
@ -365,10 +309,39 @@ const AddRelationship = ({
};
class Relationships extends Component {
state = {
supportRename: false,
};
componentDidMount() {
this.props.dispatch({ type: RESET });
this.props.dispatch(setTable(this.props.tableName));
const { dispatch, serverVersion } = this.props;
dispatch({ type: RESET });
dispatch(setTable(this.props.tableName));
if (serverVersion) {
this.checkRenameSupport(serverVersion);
}
}
componentWillReceiveProps(nextProps) {
if (
nextProps.serverVersion &&
nextProps.serverVersion !== this.props.serverVersion
) {
this.checkRenameSupport(nextProps.serverVersion);
}
}
checkRenameSupport = serverVersion => {
try {
if (semverCheck('tableColumnRename', serverVersion)) {
this.setState({
supportRename: true,
});
}
} catch (e) {
console.error(e);
}
};
render() {
const {
tableName,
@ -432,26 +405,36 @@ class Relationships extends Component {
{getObjArrayRelationshipList(tableSchema.relationships).map(
(rel, i) => {
const column1 = rel.objRel ? (
relationshipView(
dispatch,
tableName,
rel.objRel.rel_name,
findAllFromRel(allSchemas, tableSchema, rel.objRel),
true,
tableStyles
)
<RelationshipEditor
dispatch={dispatch}
tableName={tableName}
relName={rel.objRel.rel_name}
relConfig={findAllFromRel(
allSchemas,
tableSchema,
rel.objRel
)}
isObjRel
tableStyles={tableStyles}
allowRename={this.state.supportRename}
/>
) : (
<td />
);
const column2 = rel.arrRel ? (
relationshipView(
dispatch,
tableName,
rel.arrRel.rel_name,
findAllFromRel(allSchemas, tableSchema, rel.arrRel),
false,
tableStyles
)
<RelationshipEditor
dispatch={dispatch}
tableName={tableName}
relName={rel.arrRel.rel_name}
relConfig={findAllFromRel(
allSchemas,
tableSchema,
rel.arrRel
)}
isObjRel={false}
tableStyles={tableStyles}
allowRename={this.state.supportRename}
/>
) : (
<td />
);
@ -565,12 +548,14 @@ Relationships.propTypes = {
lastFormError: PropTypes.object,
lastSuccess: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
serverVersion: PropTypes.string,
};
const mapStateToProps = (state, ownProps) => ({
tableName: ownProps.params.table,
allSchemas: state.tables.allSchemas,
migrationMode: state.main.migrationMode,
serverVersion: state.main.serverVersion,
currentSchema: state.tables.currentSchema,
schemaList: state.tables.schemaList,
...state.tables.modify,

View File

@ -2,12 +2,14 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ViewHeader from '../TableBrowseRows/ViewHeader';
import { RESET } from '../TableModify/ModifyActions';
import { deleteRelMigrate, addNewRelClicked } from './Actions';
import { addNewRelClicked } from './Actions';
import { findAllFromRel } from '../utils';
import { setTable, UPDATE_REMOTE_SCHEMA_MANUAL_REL } from '../DataActions';
import AddRelationship from './AddManualRelationship';
import Button from '../../../Common/Button/Button';
import RelationshipEditor from './RelationshipEditor';
import semverCheck from '../../../../helpers/semver';
/* Gets the complete list of relationships and converts it to a list of object, which looks like so :
{
@ -31,70 +33,44 @@ const getObjArrayRelationshipList = relationships => {
return requiredList;
};
/* This function sets the styling to the way the relationship looks, for eg: id -> user::user_id */
const getRelationshipLine = (isObjRel, lcol, rcol, rTable) => {
const getGrayText = value => <i>{value}</i>;
return isObjRel ? (
<span>
&nbsp;
{getGrayText(lcol)}
&nbsp;&nbsp;&rarr;&nbsp;&nbsp;
{rTable} :: {rcol}
</span>
) : (
<span>
&nbsp;
{rTable} :: {rcol}
&nbsp;&nbsp;&rarr;&nbsp;&nbsp;
{getGrayText(lcol)}
</span>
);
};
const relationshipView = (
dispatch,
tableName,
relName,
{ lcol, rtable, rcol },
isObjRel,
tableStyles
) => {
const onDelete = e => {
e.preventDefault();
const isOk = confirm('Are you sure?');
if (isOk) {
dispatch(
deleteRelMigrate(tableName, relName, lcol, rtable, rcol, isObjRel)
);
}
};
return (
<td>
<div>
<Button size="sm" color="red" onClick={onDelete}>
Remove
</Button>
&nbsp;
<b>{relName}</b>
<div className={tableStyles.relationshipTopPadding}>
{getRelationshipLine(isObjRel, lcol, rcol, rtable)}
</div>
</div>
</td>
);
};
class RelationshipsView extends Component {
state = {
supportRename: false,
};
componentDidMount() {
this.props.dispatch({ type: RESET });
this.props.dispatch(setTable(this.props.tableName));
const { dispatch, serverVersion, currentSchema, tableName } = this.props;
dispatch({ type: RESET });
dispatch(setTable(tableName));
// Sourcing the current schema into manual relationship
this.props.dispatch({
dispatch({
type: UPDATE_REMOTE_SCHEMA_MANUAL_REL,
data: this.props.currentSchema,
data: currentSchema,
});
if (serverVersion) {
this.checkRenameSupport(serverVersion);
}
}
componentWillReceiveProps(nextProps) {
if (
nextProps.serverVersion &&
nextProps.serverVersion !== this.props.serverVersion
) {
this.checkRenameSupport(nextProps.serverVersion);
}
}
checkRenameSupport = serverVersion => {
try {
if (semverCheck('tableColumnRename', serverVersion)) {
this.setState({
supportRename: true,
});
}
} catch (e) {
console.error(e);
}
};
render() {
const {
tableName,
@ -159,26 +135,36 @@ class RelationshipsView extends Component {
{getObjArrayRelationshipList(tableSchema.relationships).map(
(rel, i) => {
const column1 = rel.objRel ? (
relationshipView(
dispatch,
tableName,
rel.objRel.rel_name,
findAllFromRel(allSchemas, tableSchema, rel.objRel),
true,
tableStyles
)
<RelationshipEditor
dispatch={dispatch}
tableName={tableName}
relName={rel.objRel.rel_name}
relConfig={findAllFromRel(
allSchemas,
tableSchema,
rel.objRel
)}
isObjRel
tableStyles={tableStyles}
allowRename={this.state.supportRename}
/>
) : (
<td />
);
const column2 = rel.arrRel ? (
relationshipView(
dispatch,
tableName,
rel.arrRel.rel_name,
findAllFromRel(allSchemas, tableSchema, rel.arrRel),
false,
tableStyles
)
<RelationshipEditor
dispatch={dispatch}
tableName={tableName}
relName={rel.arrRel.rel_name}
relConfig={findAllFromRel(
allSchemas,
tableSchema,
rel.arrRel
)}
isObjRel={false}
tableStyles={tableStyles}
allowRename={this.state.supportRename}
/>
) : (
<td />
);
@ -263,6 +249,7 @@ RelationshipsView.propTypes = {
lastFormError: PropTypes.object,
lastSuccess: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
serverVersion: PropTypes.string,
};
const mapStateToProps = (state, ownProps) => ({
@ -270,6 +257,7 @@ const mapStateToProps = (state, ownProps) => ({
allSchemas: state.tables.allSchemas,
currentSchema: state.tables.currentSchema,
migrationMode: state.main.migrationMode,
serverVersion: state.main.serverVersion,
schemaList: state.tables.schemaList,
...state.tables.modify,
});

View File

@ -0,0 +1,21 @@
import React from 'react';
/* This function sets the styling to the way the relationship looks, for eg: id -> user::user_id */
export const getRelationshipLine = (isObjRel, lcol, rcol, rTable) => {
const finalRTable = rTable.name ? rTable.name : rTable;
return isObjRel ? (
<span>
&nbsp;
{lcol.join(',')}
&nbsp;&nbsp;&rarr;&nbsp;&nbsp;
{rTable} :: {rcol.join(',')}
</span>
) : (
<span>
&nbsp;
{finalRTable} :: {rcol.join(',')}
&nbsp;&nbsp;&rarr;&nbsp;&nbsp;
{lcol.join(',')}
</span>
);
};

View File

@ -1,3 +1,11 @@
const tabNameMap = {
view: 'Browse Rows',
insert: 'Insert Row',
modify: 'Modify',
relationships: 'Relationships',
permissions: 'Permissions',
};
const ordinalColSort = (a, b) => {
if (a.ordinal_position < b.ordinal_position) {
return -1;
@ -137,4 +145,5 @@ export {
getIngForm,
escapeRegExp,
getTableName,
tabNameMap,
};

View File

@ -14,6 +14,7 @@ const componentsSemver = {
insertPermRestrictColumns: '1.0.0-alpha28',
permHideUpsertSection: '1.0.0-alpha32',
customFunctionSection: '1.0.0-alpha36',
tableColumnRename: '1.0.0-alpha39',
triggerRetryTimeout: '1.0.0-alpha38',
permUpdatePresets: '1.0.0-alpha38',
};

View File

@ -166,8 +166,11 @@ library
, Hasura.RQL.DDL.Permission.Triggers
, Hasura.RQL.DDL.Permission
, Hasura.RQL.DDL.Relationship
, Hasura.RQL.DDL.Relationship.Rename
, Hasura.RQL.DDL.Relationship.Types
, Hasura.RQL.DDL.QueryTemplate
, Hasura.RQL.DDL.Schema.Table
, Hasura.RQL.DDL.Schema.Rename
, Hasura.RQL.DDL.Schema.Function
, Hasura.RQL.DDL.Schema.Diff
, Hasura.RQL.DDL.Metadata
@ -233,6 +236,7 @@ library
, Hasura.Logging
, Network.URI.Extended
, Ops
, Migrate
other-modules: Hasura.Server.Auth.JWT.Internal
, Hasura.Server.Auth.JWT.Logging
@ -318,7 +322,8 @@ executable graphql-engine
, connection
, string-conversions
other-modules: Ops
other-modules: Ops
, Migrate
if flag(developer)
ghc-prof-options: -rtsopts -fprof-auto -fno-prof-count-entries

View File

@ -1,5 +1,6 @@
module Main where
import Migrate (migrateCatalog)
import Ops
import Control.Monad.STM (atomically)

276
server/src-exec/Migrate.hs Normal file
View File

@ -0,0 +1,276 @@
module Migrate
( curCatalogVer
, migrateCatalog
)
where
import Data.Time.Clock (UTCTime)
import Language.Haskell.TH.Syntax (Q, TExp, unTypeQ)
import Hasura.Prelude
import Hasura.RQL.DDL.Schema.Table
import Hasura.RQL.Types
import Hasura.Server.Query
import qualified Data.Aeson as A
import qualified Data.Text as T
import qualified Data.Yaml.TH as Y
import qualified Database.PG.Query as Q
curCatalogVer :: T.Text
curCatalogVer = "10"
migrateMetadata
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> Bool -> RQLQuery -> m ()
migrateMetadata buildSC rqlQuery = do
-- Build schema cache from 'hdb_catalog' only if current
-- metadata migration depends on metadata added in previous versions
when buildSC $ buildSchemaCache
-- run the RQL query to Migrate metadata
void $ runQueryM rqlQuery
setAsSystemDefinedFor2 :: (MonadTx m) => m ()
setAsSystemDefinedFor2 =
liftTx $ Q.catchE defaultTxErrorHandler $
Q.multiQ [Q.sql|
UPDATE hdb_catalog.hdb_table
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND ( table_name = 'event_triggers'
OR table_name = 'event_log'
OR table_name = 'event_invocation_logs'
);
UPDATE hdb_catalog.hdb_relationship
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND ( table_name = 'event_triggers'
OR table_name = 'event_log'
OR table_name = 'event_invocation_logs'
);
|]
setAsSystemDefinedFor5 :: (MonadTx m) => m ()
setAsSystemDefinedFor5 =
liftTx $ Q.catchE defaultTxErrorHandler $
Q.multiQ [Q.sql|
UPDATE hdb_catalog.hdb_table
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND table_name = 'remote_schemas';
|]
setAsSystemDefinedFor8 :: (MonadTx m) => m ()
setAsSystemDefinedFor8 =
liftTx $ Q.catchE defaultTxErrorHandler $
Q.multiQ [Q.sql|
UPDATE hdb_catalog.hdb_table
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND ( table_name = 'hdb_function_agg'
OR table_name = 'hdb_function'
);
UPDATE hdb_catalog.hdb_relationship
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND table_name = 'hdb_function_agg';
|]
setAsSystemDefinedFor9 :: (MonadTx m) => m ()
setAsSystemDefinedFor9 =
liftTx $ Q.catchE defaultTxErrorHandler $
Q.multiQ [Q.sql|
UPDATE hdb_catalog.hdb_table
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND table_name = 'hdb_version';
|]
getCatalogVersion
:: (MonadTx m)
=> m T.Text
getCatalogVersion = do
res <- liftTx $ Q.withQE defaultTxErrorHandler [Q.sql|
SELECT version FROM hdb_catalog.hdb_version
|] () False
return $ runIdentity $ Q.getRow res
from08To1 :: (MonadTx m) => m ()
from08To1 = liftTx $ Q.catchE defaultTxErrorHandler $ do
Q.unitQ "ALTER TABLE hdb_catalog.hdb_relationship ADD COLUMN comment TEXT NULL" () False
Q.unitQ "ALTER TABLE hdb_catalog.hdb_permission ADD COLUMN comment TEXT NULL" () False
Q.unitQ "ALTER TABLE hdb_catalog.hdb_query_template ADD COLUMN comment TEXT NULL" () False
Q.unitQ [Q.sql|
UPDATE hdb_catalog.hdb_query_template
SET template_defn =
json_build_object('type', 'select', 'args', template_defn->'select');
|] () False
from1To2
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> m ()
from1To2 = do
-- Migrate database
Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_1.sql")
migrateMetadata False migrateMetadataFrom1
-- Set as system defined
setAsSystemDefinedFor2
where
migrateMetadataFrom1 =
$(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_1.yaml" :: Q (TExp RQLQuery)))
from2To3 :: (MonadTx m) => m ()
from2To3 = liftTx $ Q.catchE defaultTxErrorHandler $ do
Q.unitQ "ALTER TABLE hdb_catalog.event_triggers ADD COLUMN headers JSON" () False
Q.unitQ "ALTER TABLE hdb_catalog.event_log ADD COLUMN next_retry_at TIMESTAMP" () False
Q.unitQ "CREATE INDEX ON hdb_catalog.event_log (trigger_id)" () False
Q.unitQ "CREATE INDEX ON hdb_catalog.event_invocation_logs (event_id)" () False
-- custom resolver
from4To5
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> m ()
from4To5 = do
Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_4_to_5.sql")
migrateMetadata False migrateMetadataFrom4
-- Set as system defined
setAsSystemDefinedFor5
where
migrateMetadataFrom4 =
$(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_4_to_5.yaml" :: Q (TExp RQLQuery)))
from3To4 :: (MonadTx m) => m ()
from3To4 = liftTx $ Q.catchE defaultTxErrorHandler $ do
Q.unitQ "ALTER TABLE hdb_catalog.event_triggers ADD COLUMN configuration JSON" () False
eventTriggers <- map uncurryEventTrigger <$> Q.listQ [Q.sql|
SELECT e.name, e.definition::json, e.webhook, e.num_retries, e.retry_interval, e.headers::json
FROM hdb_catalog.event_triggers e
|] () False
forM_ eventTriggers updateEventTrigger3To4
Q.unitQ "ALTER TABLE hdb_catalog.event_triggers\
\ DROP COLUMN definition\
\, DROP COLUMN query\
\, DROP COLUMN webhook\
\, DROP COLUMN num_retries\
\, DROP COLUMN retry_interval\
\, DROP COLUMN headers" () False
where
uncurryEventTrigger (trn, Q.AltJ tDef, w, nr, rint, Q.AltJ headers) =
EventTriggerConf trn tDef (Just w) Nothing (RetryConf nr rint Nothing) headers
updateEventTrigger3To4 etc@(EventTriggerConf name _ _ _ _ _) = Q.unitQ [Q.sql|
UPDATE hdb_catalog.event_triggers
SET
configuration = $1
WHERE name = $2
|] (Q.AltJ $ A.toJSON etc, name) True
from5To6 :: (MonadTx m) => m ()
from5To6 = liftTx $ do
-- Migrate database
Q.Discard () <- Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_5_to_6.sql")
return ()
from6To7 :: (MonadTx m) => m ()
from6To7 = liftTx $ do
-- Migrate database
Q.Discard () <- Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_6_to_7.sql")
return ()
from7To8
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> m ()
from7To8 = do
-- Migrate database
Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_7_to_8.sql")
-- Migrate metadata
-- Building schema cache is required since this metadata migration
-- involves in creating object relationship to hdb_catalog.hdb_table
migrateMetadata True migrateMetadataFrom7
setAsSystemDefinedFor8
where
migrateMetadataFrom7 =
$(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_7_to_8.yaml" :: Q (TExp RQLQuery)))
-- alter hdb_version table and track it (telemetry changes)
from8To9
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> m ()
from8To9 = do
Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_8_to_9.sql")
-- Migrate metadata
migrateMetadata False migrateMetadataFrom8
-- Set as system defined
setAsSystemDefinedFor9
where
migrateMetadataFrom8 =
$(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_8_to_9.yaml" :: Q (TExp RQLQuery)))
-- alter foreign keys on hdb_relationship and hdb_permission table to have ON UPDATE CASCADE
from9To10 :: (MonadTx m) => m ()
from9To10 = liftTx $ do
-- Migrate database
Q.Discard () <- Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_9_to_10.sql")
return ()
migrateCatalog
:: (MonadTx m, CacheRWM m, MonadIO m, UserInfoM m, HasHttpManager m)
=> UTCTime -> m String
migrateCatalog migrationTime = do
preVer <- getCatalogVersion
if | preVer == curCatalogVer ->
return "already at the latest version"
| preVer == "0.8" -> from08ToCurrent
| preVer == "1" -> from1ToCurrent
| preVer == "2" -> from2ToCurrent
| preVer == "3" -> from3ToCurrent
| preVer == "4" -> from4ToCurrent
| preVer == "5" -> from5ToCurrent
| preVer == "6" -> from6ToCurrent
| preVer == "7" -> from7ToCurrent
| preVer == "8" -> from8ToCurrent
| preVer == "9" -> from9ToCurrent
| otherwise -> throw400 NotSupported $
"unsupported version : " <> preVer
where
from9ToCurrent = from9To10 >> postMigrate
from8ToCurrent = from8To9 >> from9ToCurrent
from7ToCurrent = from7To8 >> from8ToCurrent
from6ToCurrent = from6To7 >> from7ToCurrent
from5ToCurrent = from5To6 >> from6ToCurrent
from4ToCurrent = from4To5 >> from5ToCurrent
from3ToCurrent = from3To4 >> from4ToCurrent
from2ToCurrent = from2To3 >> from3ToCurrent
from1ToCurrent = from1To2 >> from2ToCurrent
from08ToCurrent = from08To1 >> from1ToCurrent
postMigrate = do
-- update the catalog version
updateVersion
-- try building the schema cache
buildSchemaCache
return $ "successfully migrated to " ++ show curCatalogVer
updateVersion =
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
UPDATE "hdb_catalog"."hdb_version"
SET "version" = $1,
"upgraded_on" = $2
|] (curCatalogVer, migrationTime) False

View File

@ -1,12 +1,12 @@
module Ops
( initCatalogSafe
, cleanCatalog
, migrateCatalog
, execQuery
) where
import Data.Time.Clock (UTCTime)
import Language.Haskell.TH.Syntax (Q, TExp, unTypeQ)
import Migrate (curCatalogVer)
import Hasura.Prelude
import Hasura.RQL.DDL.Schema.Table
@ -22,9 +22,6 @@ import qualified Data.Yaml.TH as Y
import qualified Database.PG.Query as Q
import qualified Database.PG.Query.Connection as Q
curCatalogVer :: T.Text
curCatalogVer = "9"
initCatalogSafe
:: (QErrM m, UserInfoM m, CacheRWM m, MonadTx m, MonadIO m, HasHttpManager m)
=> UTCTime -> m String
@ -116,15 +113,6 @@ initCatalogStrict createSchema initTime = do
|] (Identity sn) False
migrateMetadata
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> RQLQuery -> m ()
migrateMetadata rqlQuery = do
-- build schema cache
buildSchemaCache
-- run the RQL query to migrate metadata
void $ runQueryM rqlQuery
setAllAsSystemDefined :: (MonadTx m) => m ()
setAllAsSystemDefined = liftTx $ Q.catchE defaultTxErrorHandler $ do
Q.unitQ "UPDATE hdb_catalog.hdb_table SET is_system_defined = 'true'" () False
@ -132,261 +120,12 @@ setAllAsSystemDefined = liftTx $ Q.catchE defaultTxErrorHandler $ do
Q.unitQ "UPDATE hdb_catalog.hdb_permission SET is_system_defined = 'true'" () False
Q.unitQ "UPDATE hdb_catalog.hdb_query_template SET is_system_defined = 'true'" () False
setAsSystemDefinedFor2 :: (MonadTx m) => m ()
setAsSystemDefinedFor2 =
liftTx $ Q.catchE defaultTxErrorHandler $
Q.multiQ [Q.sql|
UPDATE hdb_catalog.hdb_table
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND ( table_name = 'event_triggers'
OR table_name = 'event_log'
OR table_name = 'event_invocation_logs'
);
UPDATE hdb_catalog.hdb_relationship
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND ( table_name = 'event_triggers'
OR table_name = 'event_log'
OR table_name = 'event_invocation_logs'
);
|]
setAsSystemDefinedFor5 :: (MonadTx m) => m ()
setAsSystemDefinedFor5 =
liftTx $ Q.catchE defaultTxErrorHandler $
Q.multiQ [Q.sql|
UPDATE hdb_catalog.hdb_table
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND table_name = 'remote_schemas';
|]
setAsSystemDefinedFor8 :: (MonadTx m) => m ()
setAsSystemDefinedFor8 =
liftTx $ Q.catchE defaultTxErrorHandler $
Q.multiQ [Q.sql|
UPDATE hdb_catalog.hdb_table
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND ( table_name = 'hdb_function_agg'
OR table_name = 'hdb_function'
);
UPDATE hdb_catalog.hdb_relationship
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND table_name = 'hdb_function_agg';
|]
setAsSystemDefinedFor9 :: (MonadTx m) => m ()
setAsSystemDefinedFor9 =
liftTx $ Q.catchE defaultTxErrorHandler $
Q.multiQ [Q.sql|
UPDATE hdb_catalog.hdb_table
SET is_system_defined = 'true'
WHERE table_schema = 'hdb_catalog'
AND table_name = 'hdb_version';
|]
cleanCatalog :: (MonadTx m) => m ()
cleanCatalog = liftTx $ Q.catchE defaultTxErrorHandler $ do
-- This is where the generated views and triggers are stored
Q.unitQ "DROP SCHEMA IF EXISTS hdb_views CASCADE" () False
Q.unitQ "DROP SCHEMA hdb_catalog CASCADE" () False
getCatalogVersion
:: (MonadTx m)
=> m T.Text
getCatalogVersion = do
res <- liftTx $ Q.withQE defaultTxErrorHandler [Q.sql|
SELECT version FROM hdb_catalog.hdb_version
|] () False
return $ runIdentity $ Q.getRow res
from08To1 :: (MonadTx m) => m ()
from08To1 = liftTx $ Q.catchE defaultTxErrorHandler $ do
Q.unitQ "ALTER TABLE hdb_catalog.hdb_relationship ADD COLUMN comment TEXT NULL" () False
Q.unitQ "ALTER TABLE hdb_catalog.hdb_permission ADD COLUMN comment TEXT NULL" () False
Q.unitQ "ALTER TABLE hdb_catalog.hdb_query_template ADD COLUMN comment TEXT NULL" () False
Q.unitQ [Q.sql|
UPDATE hdb_catalog.hdb_query_template
SET template_defn =
json_build_object('type', 'select', 'args', template_defn->'select');
|] () False
from1To2
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> m ()
from1To2 = do
-- migrate database
Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_1.sql")
migrateMetadata migrateMetadataFrom1
-- set as system defined
setAsSystemDefinedFor2
where
migrateMetadataFrom1 =
$(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_1.yaml" :: Q (TExp RQLQuery)))
from2To3 :: (MonadTx m) => m ()
from2To3 = liftTx $ Q.catchE defaultTxErrorHandler $ do
Q.unitQ "ALTER TABLE hdb_catalog.event_triggers ADD COLUMN headers JSON" () False
Q.unitQ "ALTER TABLE hdb_catalog.event_log ADD COLUMN next_retry_at TIMESTAMP" () False
Q.unitQ "CREATE INDEX ON hdb_catalog.event_log (trigger_id)" () False
Q.unitQ "CREATE INDEX ON hdb_catalog.event_invocation_logs (event_id)" () False
-- custom resolver
from4To5
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> m ()
from4To5 = do
Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_4_to_5.sql")
migrateMetadata migrateMetadataFrom4
-- set as system defined
setAsSystemDefinedFor5
where
migrateMetadataFrom4 =
$(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_4_to_5.yaml" :: Q (TExp RQLQuery)))
from3To4 :: (MonadTx m) => m ()
from3To4 = liftTx $ Q.catchE defaultTxErrorHandler $ do
Q.unitQ "ALTER TABLE hdb_catalog.event_triggers ADD COLUMN configuration JSON" () False
eventTriggers <- map uncurryEventTrigger <$> Q.listQ [Q.sql|
SELECT e.name, e.definition::json, e.webhook, e.num_retries, e.retry_interval, e.headers::json
FROM hdb_catalog.event_triggers e
|] () False
forM_ eventTriggers updateEventTrigger3To4
Q.unitQ "ALTER TABLE hdb_catalog.event_triggers\
\ DROP COLUMN definition\
\, DROP COLUMN query\
\, DROP COLUMN webhook\
\, DROP COLUMN num_retries\
\, DROP COLUMN retry_interval\
\, DROP COLUMN headers" () False
where
uncurryEventTrigger (trn, Q.AltJ tDef, w, nr, rint, Q.AltJ headers) =
EventTriggerConf trn tDef (Just w) Nothing (RetryConf nr rint Nothing) headers
updateEventTrigger3To4 etc@(EventTriggerConf name _ _ _ _ _) = Q.unitQ [Q.sql|
UPDATE hdb_catalog.event_triggers
SET
configuration = $1
WHERE name = $2
|] (Q.AltJ $ A.toJSON etc, name) True
from5To6 :: (MonadTx m) => m ()
from5To6 = liftTx $ do
-- migrate database
Q.Discard () <- Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_5_to_6.sql")
return ()
from6To7 :: (MonadTx m) => m ()
from6To7 = liftTx $ do
-- migrate database
Q.Discard () <- Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_6_to_7.sql")
return ()
from7To8
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> m ()
from7To8 = do
-- migrate database
Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_7_to_8.sql")
-- migrate metadata
migrateMetadata migrateMetadataFrom7
setAsSystemDefinedFor8
where
migrateMetadataFrom7 =
$(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_7_to_8.yaml" :: Q (TExp RQLQuery)))
-- alter hdb_version table and track it (telemetry changes)
from8To9
:: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m)
=> m ()
from8To9 = do
Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler
$(Q.sqlFromFile "src-rsr/migrate_from_8_to_9.sql")
void $ runQueryM migrateMetadataFrom8
-- set as system defined
setAsSystemDefinedFor9
where
migrateMetadataFrom8 =
$(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_8_to_9.yaml" :: Q (TExp RQLQuery)))
migrateCatalog
:: (MonadTx m, CacheRWM m, MonadIO m, UserInfoM m, HasHttpManager m)
=> UTCTime -> m String
migrateCatalog migrationTime = do
preVer <- getCatalogVersion
if | preVer == curCatalogVer ->
return "already at the latest version"
| preVer == "0.8" -> from08ToCurrent
| preVer == "1" -> from1ToCurrent
| preVer == "2" -> from2ToCurrent
| preVer == "3" -> from3ToCurrent
| preVer == "4" -> from4ToCurrent
| preVer == "5" -> from5ToCurrent
| preVer == "6" -> from6ToCurrent
| preVer == "7" -> from7ToCurrent
| preVer == "8" -> from8ToCurrent
| otherwise -> throw400 NotSupported $
"unsupported version : " <> preVer
where
from8ToCurrent = do
from8To9
postMigrate
from7ToCurrent = do
from7To8
from8ToCurrent
from6ToCurrent = do
from6To7
from7ToCurrent
from5ToCurrent = do
from5To6
from6ToCurrent
from4ToCurrent = do
from4To5
from5ToCurrent
from3ToCurrent = do
from3To4
from4ToCurrent
from2ToCurrent = do
from2To3
from3ToCurrent
from1ToCurrent = do
from1To2
from2ToCurrent
from08ToCurrent = do
from08To1
from1ToCurrent
postMigrate = do
-- update the catalog version
updateVersion
-- try building the schema cache
buildSchemaCache
return $ "successfully migrated to " ++ show curCatalogVer
updateVersion =
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
UPDATE "hdb_catalog"."hdb_version"
SET "version" = $1,
"upgraded_on" = $2
|] (curCatalogVer, migrationTime) False
execQuery
:: (MonadTx m, CacheRWM m, MonadIO m, UserInfoM m, HasHttpManager m)
=> BL.ByteString -> m BL.ByteString

View File

@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Hasura.RQL.DDL.Metadata
( TableMeta

View File

@ -38,6 +38,7 @@ module Hasura.RQL.DDL.Permission
, addPermP1
, addPermP2
, dropView
, DropPerm
, runDropPerm

View File

@ -114,6 +114,21 @@ savePermToCatalog pt (QualifiedObject sn tn) (PermDef rn qdef mComment) =
VALUES ($1, $2, $3, $4, $5 :: jsonb, $6)
|] (sn, tn, rn, permTypeToCode pt, Q.AltJ qdef, mComment) True
updatePermDefInCatalog
:: (ToJSON a)
=> PermType
-> QualifiedTable
-> RoleName
-> a
-> Q.TxE QErr ()
updatePermDefInCatalog pt (QualifiedObject sn tn) rn qdef =
Q.unitQE defaultTxErrorHandler [Q.sql|
UPDATE hdb_catalog.hdb_permission
SET perm_def = $1 :: jsonb
WHERE table_schema = $2 AND table_name = $3
AND role_name = $4 AND perm_type = $5
|] (Q.AltJ qdef, sn, tn, rn, permTypeToCode pt) True
dropPermFromCatalog
:: QualifiedTable
-> RoleName

View File

@ -1,89 +1,32 @@
module Hasura.RQL.DDL.Relationship where
module Hasura.RQL.DDL.Relationship
( objRelP2Setup
, objRelP2
, arrRelP2Setup
, arrRelP2
, delRelFromCatalog
, validateRelP1
, runCreateObjRel
, runCreateArrRel
, runDropRel
, runSetRelComment
, module Hasura.RQL.DDL.Relationship.Types
)
where
import qualified Database.PG.Query as Q
import qualified Database.PG.Query as Q
import Hasura.Prelude
import Hasura.RQL.DDL.Deps
import Hasura.RQL.DDL.Permission (purgePerm)
import Hasura.RQL.DDL.Permission (purgePerm)
import Hasura.RQL.DDL.Relationship.Types
import Hasura.RQL.Types
import Hasura.SQL.Types
import Data.Aeson.Casing
import Data.Aeson.TH
import Data.Aeson.Types
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Tuple (swap)
import Instances.TH.Lift ()
import Language.Haskell.TH.Syntax (Lift)
data RelDef a
= RelDef
{ rdName :: !RelName
, rdUsing :: !a
, rdComment :: !(Maybe T.Text)
} deriving (Show, Eq, Lift)
$(deriveFromJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''RelDef)
instance (ToJSON a) => ToJSON (RelDef a) where
toJSON = object . toAesonPairs
instance (ToJSON a) => ToAesonPairs (RelDef a) where
toAesonPairs (RelDef rn ru rc) =
[ "name" .= rn
, "using" .= ru
, "comment" .= rc
]
data RelManualConfig
= RelManualConfig
{ rmTable :: !QualifiedTable
, rmColumns :: !(M.Map PGCol PGCol)
} deriving (Show, Eq, Lift)
instance FromJSON RelManualConfig where
parseJSON (Object v) =
RelManualConfig
<$> v .: "remote_table"
<*> v .: "column_mapping"
parseJSON _ =
fail "manual_configuration should be an object"
instance ToJSON RelManualConfig where
toJSON (RelManualConfig qt cm) =
object [ "remote_table" .= qt
, "column_mapping" .= cm
]
data RelUsing a b
= RUFKeyOn a
| RUManual b
deriving (Show, Eq, Lift)
instance (ToJSON a, ToJSON b) => ToJSON (RelUsing a b) where
toJSON (RUFKeyOn fkey) =
object [ "foreign_key_constraint_on" .= fkey ]
toJSON (RUManual manual) =
object [ "manual_configuration" .= manual ]
instance (FromJSON a, FromJSON b) => FromJSON (RelUsing a b) where
parseJSON (Object o) = do
let fkeyOnM = HM.lookup "foreign_key_constraint_on" o
manualM = HM.lookup "manual_configuration" o
let msgFrag = "one of foreign_key_constraint_on/manual_configuration should be present"
case (fkeyOnM, manualM) of
(Nothing, Nothing) -> fail $ "atleast " <> msgFrag
(Just a, Nothing) -> RUFKeyOn <$> parseJSON a
(Nothing, Just b) -> RUManual <$> parseJSON b
_ -> fail $ "only " <> msgFrag
parseJSON _ =
fail "using should be an object"
newtype ObjRelManualConfig =
ObjRelManualConfig { getObjRelMapping :: RelManualConfig }
deriving (Show, Eq, FromJSON, ToJSON, Lift)
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Tuple (swap)
import Instances.TH.Lift ()
validateManualConfig
:: (QErrM m, CacheRM m)
@ -134,11 +77,6 @@ checkForColConfilct tabInfo f =
]
Nothing -> return ()
type ObjRelUsing = RelUsing PGCol ObjRelManualConfig
type ObjRelDef = RelDef ObjRelUsing
type CreateObjRel = WithTable ObjRelDef
objRelP1
:: (QErrM m, CacheRM m)
=> TableInfo
@ -230,22 +168,6 @@ runCreateObjRel defn = do
createObjRelP1 defn
createObjRelP2 defn
data ArrRelUsingFKeyOn
= ArrRelUsingFKeyOn
{ arufTable :: !QualifiedTable
, arufColumn :: !PGCol
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 4 snakeCase){omitNothingFields=True} ''ArrRelUsingFKeyOn)
newtype ArrRelManualConfig =
ArrRelManualConfig { getArrRelMapping :: RelManualConfig }
deriving (Show, Eq, FromJSON, ToJSON, Lift)
type ArrRelUsing = RelUsing ArrRelUsingFKeyOn ArrRelManualConfig
type ArrRelDef = RelDef ArrRelUsing
type CreateArrRel = WithTable ArrRelDef
createArrRelP1 :: (UserInfoM m, QErrM m, CacheRM m) => CreateArrRel -> m ()
createArrRelP1 (WithTable qt rd) = do
adminOnly
@ -333,15 +255,6 @@ runCreateArrRel defn = do
createArrRelP1 defn
createArrRelP2 defn
data DropRel
= DropRel
{ drTable :: !QualifiedTable
, drRelationship :: !RelName
, drCascade :: !(Maybe Bool)
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''DropRel)
dropRelP1 :: (UserInfoM m, QErrM m, CacheRM m) => DropRel -> m [SchemaObjId]
dropRelP1 (DropRel qt rn cascade) = do
adminOnly
@ -390,20 +303,13 @@ delRelFromCatalog (QualifiedObject sn tn) rn =
AND rel_name = $3
|] (sn, tn, rn) True
data SetRelComment
= SetRelComment
{ arTable :: !QualifiedTable
, arRelationship :: !RelName
, arComment :: !(Maybe T.Text)
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''SetRelComment)
setRelCommentP1 :: (UserInfoM m, QErrM m, CacheRM m) => SetRelComment -> m ()
setRelCommentP1 (SetRelComment qt rn _) = do
validateRelP1
:: (UserInfoM m, QErrM m, CacheRM m)
=> QualifiedTable -> RelName -> m RelInfo
validateRelP1 qt rn = do
adminOnly
tabInfo <- askTabInfo qt
void $ askRelType (tiFieldInfoMap tabInfo) rn ""
askRelType (tiFieldInfoMap tabInfo) rn ""
setRelCommentP2
:: (QErrM m, MonadTx m)
@ -416,8 +322,10 @@ runSetRelComment
:: (QErrM m, CacheRWM m, MonadTx m , UserInfoM m)
=> SetRelComment -> m RespBody
runSetRelComment defn = do
setRelCommentP1 defn
void $ validateRelP1 qt rn
setRelCommentP2 defn
where
SetRelComment qt rn _ = defn
setRelComment :: SetRelComment
-> Q.TxE QErr ()

View File

@ -0,0 +1,42 @@
module Hasura.RQL.DDL.Relationship.Rename
(runRenameRel)
where
import Hasura.Prelude
import Hasura.RQL.DDL.Relationship (validateRelP1)
import Hasura.RQL.DDL.Relationship.Types
import Hasura.RQL.DDL.Schema.Rename (renameRelInCatalog)
import Hasura.RQL.DDL.Schema.Table (buildSchemaCache)
import Hasura.RQL.Types
import Hasura.SQL.Types
import qualified Data.HashMap.Strict as HM
renameRelP2
:: (QErrM m, MonadTx m, CacheRWM m, MonadIO m, HasHttpManager m)
=> QualifiedTable -> RelName -> RelInfo -> m ()
renameRelP2 qt newRN relInfo = do
tabInfo <- askTabInfo qt
-- check for conflicts in fieldInfoMap
case HM.lookup (fromRel newRN) $ tiFieldInfoMap tabInfo of
Nothing -> return ()
Just _ ->
throw400 AlreadyExists $ "cannot rename relationship " <> oldRN
<<> " to " <> newRN <<> " in table " <> qt <<>
" as a column/relationship with the name already exists"
-- update catalog
renameRelInCatalog qt oldRN newRN
-- update schema cache
buildSchemaCache
where
oldRN = riName relInfo
runRenameRel
:: (QErrM m, CacheRWM m, MonadTx m , UserInfoM m, MonadIO m, HasHttpManager m)
=> RenameRel -> m RespBody
runRenameRel defn = do
ri <- validateRelP1 qt rn
renameRelP2 qt newRN ri
return successMsg
where
RenameRel qt rn newRN = defn

View File

@ -0,0 +1,130 @@
module Hasura.RQL.DDL.Relationship.Types where
import Hasura.Prelude
import Hasura.RQL.Types
import Hasura.SQL.Types
import Data.Aeson.Casing
import Data.Aeson.TH
import Data.Aeson.Types
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Instances.TH.Lift ()
import Language.Haskell.TH.Syntax (Lift)
data RelDef a
= RelDef
{ rdName :: !RelName
, rdUsing :: !a
, rdComment :: !(Maybe T.Text)
} deriving (Show, Eq, Lift)
$(deriveFromJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''RelDef)
instance (ToJSON a) => ToJSON (RelDef a) where
toJSON = object . toAesonPairs
instance (ToJSON a) => ToAesonPairs (RelDef a) where
toAesonPairs (RelDef rn ru rc) =
[ "name" .= rn
, "using" .= ru
, "comment" .= rc
]
data RelManualConfig
= RelManualConfig
{ rmTable :: !QualifiedTable
, rmColumns :: !(M.Map PGCol PGCol)
} deriving (Show, Eq, Lift)
instance FromJSON RelManualConfig where
parseJSON (Object v) =
RelManualConfig
<$> v .: "remote_table"
<*> v .: "column_mapping"
parseJSON _ =
fail "manual_configuration should be an object"
instance ToJSON RelManualConfig where
toJSON (RelManualConfig qt cm) =
object [ "remote_table" .= qt
, "column_mapping" .= cm
]
data RelUsing a b
= RUFKeyOn a
| RUManual b
deriving (Show, Eq, Lift)
instance (ToJSON a, ToJSON b) => ToJSON (RelUsing a b) where
toJSON (RUFKeyOn fkey) =
object [ "foreign_key_constraint_on" .= fkey ]
toJSON (RUManual manual) =
object [ "manual_configuration" .= manual ]
instance (FromJSON a, FromJSON b) => FromJSON (RelUsing a b) where
parseJSON (Object o) = do
let fkeyOnM = HM.lookup "foreign_key_constraint_on" o
manualM = HM.lookup "manual_configuration" o
let msgFrag = "one of foreign_key_constraint_on/manual_configuration should be present"
case (fkeyOnM, manualM) of
(Nothing, Nothing) -> fail $ "atleast " <> msgFrag
(Just a, Nothing) -> RUFKeyOn <$> parseJSON a
(Nothing, Just b) -> RUManual <$> parseJSON b
_ -> fail $ "only " <> msgFrag
parseJSON _ =
fail "using should be an object"
newtype ArrRelManualConfig =
ArrRelManualConfig { getArrRelMapping :: RelManualConfig }
deriving (Show, Eq, FromJSON, ToJSON, Lift)
data ArrRelUsingFKeyOn
= ArrRelUsingFKeyOn
{ arufTable :: !QualifiedTable
, arufColumn :: !PGCol
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 4 snakeCase){omitNothingFields=True} ''ArrRelUsingFKeyOn)
type ArrRelUsing = RelUsing ArrRelUsingFKeyOn ArrRelManualConfig
type ArrRelDef = RelDef ArrRelUsing
type CreateArrRel = WithTable ArrRelDef
newtype ObjRelManualConfig =
ObjRelManualConfig { getObjRelMapping :: RelManualConfig }
deriving (Show, Eq, FromJSON, ToJSON, Lift)
type ObjRelUsing = RelUsing PGCol ObjRelManualConfig
type ObjRelDef = RelDef ObjRelUsing
type CreateObjRel = WithTable ObjRelDef
data DropRel
= DropRel
{ drTable :: !QualifiedTable
, drRelationship :: !RelName
, drCascade :: !(Maybe Bool)
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''DropRel)
data SetRelComment
= SetRelComment
{ arTable :: !QualifiedTable
, arRelationship :: !RelName
, arComment :: !(Maybe T.Text)
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''SetRelComment)
data RenameRel
= RenameRel
{ rrTable :: !QualifiedTable
, rrName :: !RelName
, rrNewName :: !RelName
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 2 snakeCase) ''RenameRel)

View File

@ -27,6 +27,7 @@ import Hasura.SQL.Types
import qualified Database.PG.Query as Q
import Control.Arrow ((***))
import Data.Aeson.Casing
import Data.Aeson.TH
@ -166,8 +167,7 @@ getTableDiff oldtm newtm =
= PGColInfo colName colType isNullable
alteredCols =
flip map (filter (uncurry (/=)) existingCols) $ \(pcmo, pcmn) ->
(pcmToPci pcmo, pcmToPci pcmn)
flip map (filter (uncurry (/=)) existingCols) $ pcmToPci *** pcmToPci
droppedFKeyConstraints = map cmName $
filter (isForeignKey . cmType) $ getDifference cmOid
@ -225,7 +225,7 @@ getSchemaChangeDeps schemaDiff = do
where
SchemaDiff droppedTables alteredTables = schemaDiff
isDirectDep (SOTableObj tn _) = tn `HS.member` (HS.fromList droppedTables)
isDirectDep (SOTableObj tn _) = tn `HS.member` HS.fromList droppedTables
isDirectDep _ = False
data FunctionMeta

View File

@ -0,0 +1,381 @@
module Hasura.RQL.DDL.Schema.Rename
( renameTableInCatalog
, renameColInCatalog
, renameRelInCatalog
)
where
import Control.Arrow ((***))
import Hasura.Prelude
import Hasura.RQL.DDL.Permission
import Hasura.RQL.DDL.Permission.Internal
import Hasura.RQL.DDL.Relationship.Types
import Hasura.RQL.Types
import Hasura.SQL.Types
import qualified Data.HashMap.Strict as M
import qualified Data.Map.Strict as Map
import qualified Database.PG.Query as Q
import Data.Aeson
data RenameItem a
= RenameItem
{ _riTable :: !QualifiedTable
, _riOld :: !a
, _riNew :: !a
} deriving (Show, Eq)
type RenameCol = RenameItem PGCol
data RenameField
= RFCol !RenameCol
| RFRel !(RenameItem RelName)
deriving (Show, Eq)
type RenameTable = (QualifiedTable, QualifiedTable)
otherDeps :: QErrM m => Text -> SchemaObjId -> m ()
otherDeps errMsg = \case
SOQTemplate name ->
throw400 NotSupported $
"found dependant query template " <> name <<> "; " <> errMsg
d ->
throw500 $ "unexpected dependancy "
<> reportSchemaObj d <> "; " <> errMsg
renameTableInCatalog
:: (MonadTx m, CacheRM m)
=> QualifiedTable -> QualifiedTable -> m ()
renameTableInCatalog newQT oldQT = do
sc <- askSchemaCache
let allDeps = getDependentObjs sc $ SOTable oldQT
-- update all dependant schema objects
forM_ allDeps $ \case
SOTableObj refQT (TORel rn) ->
updateRelDefs refQT rn (oldQT, newQT)
-- table names are not specified in permission definitions
SOTableObj _ (TOPerm _ _) -> return ()
d -> otherDeps errMsg d
-- -- Update table name in hdb_catalog
liftTx $ Q.catchE defaultTxErrorHandler updateTableInCatalog
where
QualifiedObject nsn ntn = newQT
QualifiedObject osn otn = oldQT
errMsg = "cannot rename table " <> oldQT <<> " to " <>> newQT
updateTableInCatalog =
Q.unitQ [Q.sql|
UPDATE "hdb_catalog"."hdb_table"
SET table_schema = $1, table_name = $2
WHERE table_schema = $3 AND table_name = $4
|] (nsn, ntn, osn, otn) False
renameColInCatalog
:: (MonadTx m, CacheRM m)
=> PGCol -> PGCol -> QualifiedTable -> TableInfo -> m ()
renameColInCatalog oCol nCol qt ti = do
sc <- askSchemaCache
-- Check if any relation exists with new column name
assertFldNotExists
-- Fetch dependent objects
let depObjs = getDependentObjs sc $ SOTableObj qt $ TOCol oCol
renameFld = RFCol $ RenameItem qt oCol nCol
-- Update dependent objects
forM_ depObjs $ \case
SOTableObj refQT (TOPerm role pt) ->
updatePermFlds refQT role pt renameFld
SOTableObj refQT (TORel rn) ->
updateColInRel refQT rn $ RenameItem qt oCol nCol
d -> otherDeps errMsg d
where
errMsg = "cannot rename column " <> oCol <<> " to " <>> nCol
assertFldNotExists =
case M.lookup (fromPGCol oCol) $ tiFieldInfoMap ti of
Just (FIRelationship _) ->
throw400 AlreadyExists $ "cannot rename column " <> oCol
<<> " to " <> nCol <<> " in table " <> qt <<>
" as a relationship with the name already exists"
_ -> return ()
renameRelInCatalog
:: (MonadTx m, CacheRM m)
=> QualifiedTable -> RelName -> RelName -> m ()
renameRelInCatalog qt oldRN newRN = do
sc <- askSchemaCache
let depObjs = getDependentObjs sc $ SOTableObj qt $ TORel oldRN
renameFld = RFRel $ RenameItem qt oldRN newRN
forM_ depObjs $ \case
SOTableObj refQT (TOPerm role pt) ->
updatePermFlds refQT role pt renameFld
d -> otherDeps errMsg d
liftTx updateRelName
where
errMsg = "cannot rename relationship " <> oldRN <<> " to " <>> newRN
QualifiedObject sn tn = qt
updateRelName =
Q.unitQE defaultTxErrorHandler [Q.sql|
UPDATE hdb_catalog.hdb_relationship
SET rel_name = $1
WHERE table_schema = $2
AND table_name = $3
AND rel_name = $4
|] (newRN, sn, tn, oldRN) True
-- update table names in relationship definition
updateRelDefs
:: (MonadTx m, CacheRM m)
=> QualifiedTable -> RelName -> RenameTable -> m ()
updateRelDefs qt rn renameTable = do
fim <- askFieldInfoMap qt
ri <- askRelType fim rn ""
case riType ri of
ObjRel -> updateObjRelDef qt rn renameTable
ArrRel -> updateArrRelDef qt rn renameTable
updateObjRelDef
:: (MonadTx m)
=> QualifiedTable -> RelName -> RenameTable -> m ()
updateObjRelDef qt rn (oldQT, newQT) = do
oldDefV <- liftTx $ getRelDef qt rn
oldDef :: ObjRelUsing <- decodeValue oldDefV
let newDef = case oldDef of
RUFKeyOn _ -> oldDef
RUManual (ObjRelManualConfig (RelManualConfig dbQT rmCols)) ->
let updQT = bool oldQT newQT $ oldQT == dbQT
in RUManual $ ObjRelManualConfig $ RelManualConfig updQT rmCols
liftTx $ updateRel qt rn $ toJSON newDef
updateArrRelDef
:: (MonadTx m)
=> QualifiedTable -> RelName -> RenameTable -> m ()
updateArrRelDef qt rn (oldQT, newQT) = do
oldDefV <- liftTx $ getRelDef qt rn
oldDef <- decodeValue oldDefV
let newDef = case oldDef of
RUFKeyOn (ArrRelUsingFKeyOn dbQT c) ->
let updQT = getUpdQT dbQT
in RUFKeyOn $ ArrRelUsingFKeyOn updQT c
RUManual (ArrRelManualConfig (RelManualConfig dbQT rmCols)) ->
let updQT = getUpdQT dbQT
in RUManual $ ArrRelManualConfig $ RelManualConfig updQT rmCols
liftTx $ updateRel qt rn $ toJSON newDef
where
getUpdQT dbQT = bool oldQT newQT $ oldQT == dbQT
-- | update fields in premissions
updatePermFlds :: (MonadTx m, CacheRM m)
=> QualifiedTable -> RoleName -> PermType -> RenameField -> m ()
updatePermFlds refQT rn pt rf = do
Q.AltJ pDef <- liftTx fetchPermDef
case pt of
PTInsert -> do
perm <- decodeValue pDef
updateInsPermFlds refQT rf rn perm
PTSelect -> do
perm <- decodeValue pDef
updateSelPermFlds refQT rf rn perm
PTUpdate -> do
perm <- decodeValue pDef
updateUpdPermFlds refQT rf rn perm
PTDelete -> do
perm <- decodeValue pDef
updateDelPermFlds refQT rf rn perm
where
QualifiedObject sn tn = refQT
fetchPermDef =
runIdentity . Q.getRow <$>
Q.withQE defaultTxErrorHandler [Q.sql|
SELECT perm_def::json
FROM hdb_catalog.hdb_permission
WHERE table_schema = $1
AND table_name = $2
AND role_name = $3
AND perm_type = $4
|] (sn, tn, rn, permTypeToCode pt) True
updateInsPermFlds
:: (MonadTx m, CacheRM m)
=> QualifiedTable -> RenameField -> RoleName -> InsPerm -> m ()
updateInsPermFlds qt rf rn (InsPerm chk preset cols) = do
updBoolExp <- updateBoolExp qt rf chk
liftTx $ updatePermDefInCatalog PTInsert qt rn $
InsPerm updBoolExp updPresetM updColsM
where
updPresetM = updatePreset qt rf <$> preset
updColsM = updateCols qt rf <$> cols
updateSelPermFlds
:: (MonadTx m, CacheRM m)
=> QualifiedTable -> RenameField -> RoleName -> SelPerm -> m ()
updateSelPermFlds refQT rf rn (SelPerm cols fltr limit aggAllwd) = do
updBoolExp <- updateBoolExp refQT rf fltr
liftTx $ updatePermDefInCatalog PTSelect refQT rn $
SelPerm updCols updBoolExp limit aggAllwd
where
updCols = updateCols refQT rf cols
updateUpdPermFlds
:: (MonadTx m, CacheRM m)
=> QualifiedTable -> RenameField -> RoleName -> UpdPerm -> m ()
updateUpdPermFlds refQT rf rn (UpdPerm cols preset fltr) = do
updBoolExp <- updateBoolExp refQT rf fltr
liftTx $ updatePermDefInCatalog PTUpdate refQT rn $
UpdPerm updCols updPresetM updBoolExp
where
updCols = updateCols refQT rf cols
updPresetM = updatePreset refQT rf <$> preset
updateDelPermFlds
:: (MonadTx m, CacheRM m)
=> QualifiedTable -> RenameField -> RoleName -> DelPerm -> m ()
updateDelPermFlds refQT rf rn (DelPerm fltr) = do
updBoolExp <- updateBoolExp refQT rf fltr
liftTx $ updatePermDefInCatalog PTDelete refQT rn $
DelPerm updBoolExp
updatePreset
:: QualifiedTable -> RenameField -> ColVals -> ColVals
updatePreset qt rf obj =
case rf of
RFCol (RenameItem opQT oCol nCol) ->
if qt == opQT then updatePreset' oCol nCol
else obj
_ -> obj
where
updatePreset' oCol nCol =
M.fromList updItems
where
updItems= map procObjItem $ M.toList obj
procObjItem (pgCol, v) =
let isUpdated = pgCol == oCol
updCol = bool pgCol nCol isUpdated
in (updCol, v)
updateCols
:: QualifiedTable -> RenameField -> PermColSpec -> PermColSpec
updateCols qt rf permSpec =
case rf of
RFCol (RenameItem opQT oCol nCol) ->
if qt == opQT then updateCols' oCol nCol permSpec
else permSpec
_ -> permSpec
where
updateCols' oCol nCol cols = case cols of
PCStar -> cols
PCCols c -> PCCols $ flip map c $
\col -> if col == oCol then nCol else col
updateBoolExp
:: (QErrM m, CacheRM m)
=> QualifiedTable -> RenameField -> BoolExp -> m BoolExp
updateBoolExp qt rf =
fmap BoolExp . traverse (updateColExp qt rf) . unBoolExp
updateColExp
:: (QErrM m, CacheRM m)
=> QualifiedTable -> RenameField -> ColExp-> m ColExp
updateColExp qt rf (ColExp fld val) =
ColExp updatedFld <$> updatedVal
where
updatedFld = bool fld nFld $ opQT == qt && oFld == fld
updatedVal = do
fim <- askFieldInfoMap qt
fi <- askFieldInfo fim fld
case fi of
FIColumn _ -> return val
FIRelationship ri -> do
let remTable = riRTable ri
be <- decodeValue val
ube <- updateBoolExp remTable rf be
return $ toJSON ube
(oFld, nFld, opQT) = case rf of
RFCol (RenameItem tn oCol nCol) -> (fromPGCol oCol, fromPGCol nCol, tn)
RFRel (RenameItem tn oRel nRel) -> (fromRel oRel, fromRel nRel, tn)
-- rename columns in relationship definitions
updateColInRel
:: (MonadTx m, CacheRM m)
=> QualifiedTable -> RelName -> RenameCol -> m ()
updateColInRel fromQT rn rnCol = do
fim <- askFieldInfoMap fromQT
ri <- askRelType fim rn ""
let toQT = riRTable ri
oldDefV <- liftTx $ getRelDef fromQT rn
newDefV <- case riType ri of
ObjRel -> fmap toJSON $
updateColInObjRel fromQT toQT rnCol <$> decodeValue oldDefV
ArrRel -> fmap toJSON $
updateColInArrRel fromQT toQT rnCol <$> decodeValue oldDefV
liftTx $ updateRel fromQT rn newDefV
updateColInObjRel
:: QualifiedTable -> QualifiedTable
-> RenameCol -> ObjRelUsing -> ObjRelUsing
updateColInObjRel fromQT toQT rnCol = \case
RUFKeyOn col -> RUFKeyOn $ updateColRel fromQT col rnCol
RUManual (ObjRelManualConfig manConfig) ->
RUManual $ ObjRelManualConfig $
updateRelManualConfig fromQT toQT rnCol manConfig
updateColInArrRel
:: QualifiedTable -> QualifiedTable
-> RenameCol -> ArrRelUsing -> ArrRelUsing
updateColInArrRel fromQT toQT rnCol = \case
RUFKeyOn (ArrRelUsingFKeyOn t c) ->
let updCol = updateColRel toQT c rnCol
in RUFKeyOn $ ArrRelUsingFKeyOn t updCol
RUManual (ArrRelManualConfig manConfig) ->
RUManual $ ArrRelManualConfig $
updateRelManualConfig fromQT toQT rnCol manConfig
type ColMap = Map.Map PGCol PGCol
updateColRel
:: QualifiedTable -> PGCol -> RenameCol -> PGCol
updateColRel qt col rnCol =
if opQT == qt && col == oCol then nCol else col
where
RenameItem opQT oCol nCol = rnCol
updateRelManualConfig
:: QualifiedTable -> QualifiedTable
-> RenameCol -> RelManualConfig -> RelManualConfig
updateRelManualConfig fromQT toQT rnCol manConfig =
RelManualConfig tn $ updateColMap fromQT toQT rnCol colMap
where
RelManualConfig tn colMap = manConfig
updateColMap
:: QualifiedTable -> QualifiedTable
-> RenameCol -> ColMap -> ColMap
updateColMap fromQT toQT rnCol colMap =
Map.fromList $ map (modCol fromQT *** modCol toQT) (Map.toList colMap)
where
RenameItem qt oCol nCol = rnCol
modCol colQt col = if colQt == qt && col == oCol then nCol else col
-- database functions for relationships
getRelDef :: QualifiedTable -> RelName -> Q.TxE QErr Value
getRelDef (QualifiedObject sn tn) rn =
Q.getAltJ . runIdentity . Q.getRow <$> Q.withQE defaultTxErrorHandler
[Q.sql|
SELECT rel_def::json FROM hdb_catalog.hdb_relationship
WHERE table_schema = $1 AND table_name = $2
AND rel_name = $3
|] (sn, tn, rn) True
updateRel :: QualifiedTable -> RelName -> Value -> Q.TxE QErr ()
updateRel (QualifiedObject sn tn) rn relDef =
Q.unitQE defaultTxErrorHandler [Q.sql|
UPDATE hdb_catalog.hdb_relationship
SET rel_def = $1 :: jsonb
WHERE table_schema = $2
AND table_name = $3
AND rel_name = $4
|] (Q.AltJ relDef, sn , tn, rn) True

View File

@ -10,6 +10,7 @@ import Hasura.RQL.DDL.Relationship
import Hasura.RQL.DDL.RemoteSchema
import Hasura.RQL.DDL.Schema.Diff
import Hasura.RQL.DDL.Schema.Function
import Hasura.RQL.DDL.Schema.Rename
import Hasura.RQL.DDL.Subscribe
import Hasura.RQL.DDL.Utils
import Hasura.RQL.Types
@ -30,7 +31,6 @@ import qualified Data.HashMap.Strict as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Database.PostgreSQL.LibPQ as PQ
import qualified Language.GraphQL.Draft.Syntax as G
delTableFromCatalog :: QualifiedTable -> Q.Tx ()
delTableFromCatalog (QualifiedObject sn tn) =
@ -82,7 +82,8 @@ trackExistingTableOrViewP2
trackExistingTableOrViewP2 vn isSystemDefined = do
sc <- askSchemaCache
let defGCtx = scDefaultRemoteGCtx sc
GS.checkConflictingNode defGCtx (G.Name tn)
tn = GS.qualObjectToName vn
GS.checkConflictingNode defGCtx tn
trackExistingTableOrViewP2Setup vn isSystemDefined
liftTx $ Q.catchE defaultTxErrorHandler $
@ -92,12 +93,6 @@ trackExistingTableOrViewP2 vn isSystemDefined = do
refreshGCtxMapInSchema
return successMsg
where
getSchemaN = getSchemaTxt . qSchema
getTableN = getTableTxt . qName
tn = case getSchemaN vn of
"public" -> getTableN vn
_ -> getSchemaN vn <> "_" <> getTableN vn
runTrackTableQ
:: ( QErrM m, CacheRWM m, MonadTx m
@ -134,53 +129,69 @@ purgeDep schemaObjId = case schemaObjId of
_ -> throw500 $
"unexpected dependent object : " <> reportSchemaObj schemaObjId
processTableChanges
:: (QErrM m, CacheRWM m) => TableInfo -> TableDiff -> m ()
processTableChanges :: (MonadTx m, CacheRWM m)
=> TableInfo -> TableDiff -> m Bool
processTableChanges ti tableDiff = do
when (isJust mNewName) $
throw400 NotSupported $ "table renames are not yet supported : " <>> tn
-- replace constraints
replaceConstraints
-- for all the dropped columns
forM_ droppedCols $ \droppedCol ->
-- Drop the column from the cache
delColFromCache droppedCol tn
-- In the newly added columns check that there is no conflict with relationships
forM_ addedCols $ \colInfo@(PGColInfo colName _ _) ->
case M.lookup (fromPGCol colName) $ tiFieldInfoMap ti of
Just (FIRelationship _) ->
throw400 AlreadyExists $ "cannot add column " <> colName
<<> " in table " <> tn <<>
" as a relationship with the name already exists"
_ -> addColToCache colName colInfo tn
-- If table rename occurs then don't replace constraints and
-- process dropped/added columns, because schema reload happens eventually
sc <- askSchemaCache
-- for rest of the columns
forM_ alteredCols $ \(PGColInfo oColName oColTy _, nci@(PGColInfo nColName nColTy _)) ->
if | oColName /= nColName ->
throw400 NotSupported $ "column renames are not yet supported : " <>
tn <<> "." <>> oColName
| oColTy /= nColTy -> do
let colId = SOTableObj tn $ TOCol oColName
depObjs = getDependentObjsWith (== "on_type") sc colId
if null depObjs
then updateFldInCache oColName nci
else throw400 DependencyError $ "cannot change type of column " <> oColName <<> " in table "
<> tn <<> " because of the following dependencies : " <>
reportSchemaObjs depObjs
| otherwise -> return ()
let tn = tiName ti
withOldTabName = do
-- replace constraints
replaceConstraints tn
-- for all the dropped columns
procDroppedCols tn
-- for all added columns
procAddedCols tn
-- for all altered columns
procAlteredCols sc tn
withNewTabName newTN = do
let tnGQL = GS.qualObjectToName newTN
defGCtx = scDefaultRemoteGCtx sc
-- check for GraphQL schema conflicts on new name
GS.checkConflictingNode defGCtx tnGQL
void $ procAlteredCols sc tn
-- update new table in catalog
renameTableInCatalog newTN tn
return True
maybe withOldTabName withNewTabName mNewName
where
updateFldInCache cn ci = do
delColFromCache cn tn
addColToCache cn ci tn
replaceConstraints = flip modTableInCache tn $ \tInfo ->
return $ tInfo {tiUniqOrPrimConstraints = constraints}
tn = tiName ti
TableDiff mNewName droppedCols addedCols alteredCols _ constraints = tableDiff
replaceConstraints tn = flip modTableInCache tn $ \tInfo ->
return $ tInfo {tiUniqOrPrimConstraints = constraints}
procDroppedCols tn =
forM_ droppedCols $ \droppedCol ->
-- Drop the column from the cache
delColFromCache droppedCol tn
procAddedCols tn =
-- In the newly added columns check that there is no conflict with relationships
forM_ addedCols $ \pci@(PGColInfo colName _ _) ->
case M.lookup (fromPGCol colName) $ tiFieldInfoMap ti of
Just (FIRelationship _) ->
throw400 AlreadyExists $ "cannot add column " <> colName
<<> " in table " <> tn <<>
" as a relationship with the name already exists"
_ -> addColToCache colName pci tn
procAlteredCols sc tn = fmap or $
forM alteredCols $ \(PGColInfo oColName oColTy _, PGColInfo nColName nColTy _) ->
if | oColName /= nColName -> do
renameColInCatalog oColName nColName tn ti
return True
| oColTy /= nColTy -> do
let colId = SOTableObj tn $ TOCol oColName
depObjs = getDependentObjsWith (== "on_type") sc colId
unless (null depObjs) $ throw400 DependencyError $
"cannot change type of column " <> oColName <<> " in table "
<> tn <<> " because of the following dependencies : " <>
reportSchemaObjs depObjs
return False
| otherwise -> return False
delTableAndDirectDeps
:: (QErrM m, CacheRWM m, MonadTx m) => QualifiedTable -> m ()
@ -201,14 +212,13 @@ delTableAndDirectDeps qtn@(QualifiedObject sn tn) = do
delTableFromCatalog qtn
delTableFromCache qtn
processSchemaChanges
:: (QErrM m, CacheRWM m, MonadTx m) => SchemaDiff -> m ()
processSchemaChanges :: (MonadTx m, CacheRWM m) => SchemaDiff -> m Bool
processSchemaChanges schemaDiff = do
-- Purge the dropped tables
mapM_ delTableAndDirectDeps droppedTables
-- Get schema cache
sc <- askSchemaCache
forM_ alteredTables $ \(oldQtn, tableDiff) -> do
fmap or $ forM alteredTables $ \(oldQtn, tableDiff) -> do
ti <- case M.lookup oldQtn $ scTables sc of
Just ti -> return ti
Nothing -> throw500 $ "old table metadata not found in cache : " <>> oldQtn
@ -473,24 +483,28 @@ execWithMDCheck (RunSQL t cascade _) = do
throw400 NotSupported $
"type of function " <> qf <<> " is altered to \"VOLATILE\" which is not supported now"
-- update the schema cache with the changes
processSchemaChanges schemaDiff
-- update the schema cache and hdb_catalog with the changes
reloadRequired <- processSchemaChanges schemaDiff
postSc <- askSchemaCache
-- recreate the insert permission infra
forM_ (M.elems $ scTables postSc) $ \ti -> do
let tn = tiName ti
forM_ (M.elems $ tiRolePermInfoMap ti) $ \rpi ->
maybe (return ()) (liftTx . buildInsInfra tn) $ _permIns rpi
let withReload = buildSchemaCache
withoutReload = do
postSc <- askSchemaCache
-- recreate the insert permission infra
forM_ (M.elems $ scTables postSc) $ \ti -> do
let tn = tiName ti
forM_ (M.elems $ tiRolePermInfoMap ti) $ \rpi ->
maybe (return ()) (liftTx . buildInsInfra tn) $ _permIns rpi
--recreate triggers
forM_ (M.elems $ scTables postSc) $ \ti -> do
let tn = tiName ti
cols = getCols $ tiFieldInfoMap ti
forM_ (M.toList $ tiEventTriggerInfoMap ti) $ \(trn, eti) -> do
let opsDef = etiOpsDef eti
trid = etiId eti
liftTx $ mkTriggerQ trid trn tn cols opsDef
--recreate triggers
forM_ (M.elems $ scTables postSc) $ \ti -> do
let tn = tiName ti
cols = getCols $ tiFieldInfoMap ti
forM_ (M.toList $ tiEventTriggerInfoMap ti) $ \(trn, eti) -> do
let opsDef = etiOpsDef eti
trid = etiId eti
liftTx $ mkTriggerQ trid trn tn cols opsDef
bool withoutReload withReload reloadRequired
-- refresh the gCtxMap in schema cache
refreshGCtxMapInSchema

View File

@ -772,7 +772,6 @@ getDependentObjsWith f sc objId =
where
isDependency deps = not $ HS.null $ flip HS.filter deps $
\(SchemaDependency depId reason) -> objId `induces` depId && f reason
-- induces a b : is b dependent on a
induces (SOTable tn1) (SOTable tn2) = tn1 == tn2
induces (SOTable tn1) (SOTableObj tn2 _) = tn1 == tn2

View File

@ -3,18 +3,19 @@ module Hasura.Server.Query where
import Data.Aeson
import Data.Aeson.Casing
import Data.Aeson.TH
import Language.Haskell.TH.Syntax (Lift)
import Language.Haskell.TH.Syntax (Lift)
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import qualified Data.Vector as V
import qualified Network.HTTP.Client as HTTP
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import qualified Data.Vector as V
import qualified Network.HTTP.Client as HTTP
import Hasura.Prelude
import Hasura.RQL.DDL.Metadata
import Hasura.RQL.DDL.Permission
import Hasura.RQL.DDL.QueryTemplate
import Hasura.RQL.DDL.Relationship
import Hasura.RQL.DDL.Relationship.Rename
import Hasura.RQL.DDL.RemoteSchema
import Hasura.RQL.DDL.Schema.Function
import Hasura.RQL.DDL.Schema.Table
@ -23,12 +24,12 @@ import Hasura.RQL.DML.Count
import Hasura.RQL.DML.Delete
import Hasura.RQL.DML.Insert
import Hasura.RQL.DML.QueryTemplate
import Hasura.RQL.DML.Returning (encodeJSONVector)
import Hasura.RQL.DML.Returning (encodeJSONVector)
import Hasura.RQL.DML.Select
import Hasura.RQL.DML.Update
import Hasura.RQL.Types
import qualified Database.PG.Query as Q
import qualified Database.PG.Query as Q
data RQLQuery
= RQAddExistingTableOrView !TrackTable
@ -42,6 +43,7 @@ data RQLQuery
| RQCreateArrayRelationship !CreateArrRel
| RQDropRelationship !DropRel
| RQSetRelationshipComment !SetRelComment
| RQRenameRelationship !RenameRel
| RQCreateInsertPermission !CreateInsPerm
| RQCreateSelectPermission !CreateSelPerm
@ -142,6 +144,7 @@ queryNeedsReload qi = case qi of
RQCreateArrayRelationship _ -> True
RQDropRelationship _ -> True
RQSetRelationshipComment _ -> False
RQRenameRelationship _ -> True
RQCreateInsertPermission _ -> True
RQCreateSelectPermission _ -> True
@ -201,6 +204,7 @@ runQueryM rq = withPathK "args" $ case rq of
RQCreateArrayRelationship q -> runCreateArrRel q
RQDropRelationship q -> runDropRel q
RQSetRelationshipComment q -> runSetRelComment q
RQRenameRelationship q -> runRenameRel q
RQCreateInsertPermission q -> runCreatePerm q
RQCreateSelectPermission q -> runCreatePerm q

View File

@ -45,7 +45,7 @@ CREATE TABLE hdb_catalog.hdb_relationship
is_system_defined boolean default false,
PRIMARY KEY (table_schema, table_name, rel_name),
FOREIGN KEY (table_schema, table_name) REFERENCES hdb_catalog.hdb_table(table_schema, table_name)
FOREIGN KEY (table_schema, table_name) REFERENCES hdb_catalog.hdb_table(table_schema, table_name) ON UPDATE CASCADE
);
CREATE TABLE hdb_catalog.hdb_permission
@ -59,7 +59,7 @@ CREATE TABLE hdb_catalog.hdb_permission
is_system_defined boolean default false,
PRIMARY KEY (table_schema, table_name, role_name, perm_type),
FOREIGN KEY (table_schema, table_name) REFERENCES hdb_catalog.hdb_table(table_schema, table_name)
FOREIGN KEY (table_schema, table_name) REFERENCES hdb_catalog.hdb_table(table_schema, table_name) ON UPDATE CASCADE
);
CREATE VIEW hdb_catalog.hdb_permission_agg AS

View File

@ -0,0 +1,8 @@
ALTER TABLE hdb_catalog.hdb_relationship
DROP CONSTRAINT hdb_relationship_table_schema_fkey,
ADD CONSTRAINT hdb_relationship_table_schema_fkey FOREIGN KEY (table_schema, table_name) REFERENCES hdb_catalog.hdb_table(table_schema, table_name) ON UPDATE CASCADE;
ALTER TABLE hdb_catalog.hdb_permission
DROP CONSTRAINT hdb_permission_table_schema_fkey,
ADD CONSTRAINT hdb_permission_table_schema_fkey FOREIGN KEY (table_schema, table_name) REFERENCES hdb_catalog.hdb_table(table_schema, table_name) ON UPDATE CASCADE;

View File

@ -50,9 +50,13 @@ querySpecFiles =
, "upsert_role_user_error.yaml"
]
gqlIntrospection :: FilePath
gqlIntrospection = "introspection.yaml"
gqlSpecFiles :: [FilePath]
gqlSpecFiles =
[ "introspection.yaml"
[ "insert_mutation/author.yaml"
, "introspection.yaml"
, "introspection_user_role.yaml"
, "insert_mutation/author.yaml"
, "insert_mutation/author_articles_nested.yaml"
@ -98,6 +102,9 @@ gqlSpecFiles =
, "delete_mutation/author_foreign_key_violation.yaml"
]
alterTable :: FilePath
alterTable = "alter_table.yaml"
readTestCase :: FilePath -> IO TestCase
readTestCase fpath = do
res <- Y.decodeFileEither ("test/testcases/" ++ fpath)
@ -129,6 +136,8 @@ mkSpecs :: IO (SpecWith Application)
mkSpecs = do
ddlTc <- mapM readTestCase querySpecFiles
gqlTc <- mapM readTestCase gqlSpecFiles
gqlIntrospectionTc <- readTestCase gqlIntrospection
alterTabTc <- readTestCase alterTable
return $ do
describe "version API" $
it "responds with version" $
@ -146,4 +155,11 @@ mkSpecs = do
describe "Query API" $ mapM_ mkSpec ddlTc
describe "GraphQL Introspection" $ mkSpec gqlIntrospectionTc
describe "GraphQL API" $ mapM_ mkSpec gqlTc
describe "Alter Table" $ mkSpec alterTabTc
describe "GraphQL Introspection after altering a table"
$ mkSpec gqlIntrospectionTc

View File

@ -0,0 +1,15 @@
description: Runs a bulk sql query to alter a table
url: /v1/query
status: 200
query:
type: bulk
args:
- type: run_sql
args:
sql: "ALTER TABLE dollar$test RENAME TO dollar_test"
- type: run_sql
args:
sql: "ALTER TABLE dollar_test RENAME name TO name_altered"
- type: run_sql
args:
sql: "ALTER TABLE dollar_test RENAME CONSTRAINT dollar$test_pkey TO dollar_test_pkey"

View File

@ -16,6 +16,20 @@
column_mapping:
id: author_id
#Rename relationship
- description: Rename relationship articles to articles_array
url: /v1/query
status: 200
response:
message: success
query:
type: rename_relationship
args:
table: author_view
name: articles
new_name: articles_array
#Drop relationship
- description: Drop object relationship
url: /v1/query
@ -26,4 +40,4 @@
type: drop_relationship
args:
table: author_view
relationship: articles
relationship: articles_array

View File

@ -47,6 +47,55 @@
- id
- name
#Rename object relationship
- description: Rename object relationship author to author_obj
url: /v1/query
status: 200
response:
message: success
query:
type: rename_relationship
args:
table: article
name: author
new_name: author_obj
#Select Query
- description: Nested select on article with renamed object relation
url: /v1/query
status: 200
response:
- id: 1
title: Article 1
content: Sample article content 1
author_obj:
id: 1
name: Author 1
- id: 2
title: Article 2
content: Sample article content 2
author_obj:
id: 1
name: Author 1
- id: 3
title: Article 3
content: Sample article content 3
author_obj:
id: 2
name: Author 2
query:
type: select
args:
table: article
columns:
- id
- title
- content
- name: author_obj
columns:
- id
- name
#Drop object relationship
- description: Drop object relationship
@ -60,4 +109,4 @@
type: drop_relationship
args:
table: article
relationship: author
relationship: author_obj

View File

@ -50,6 +50,56 @@
- id
- name
#Rename object relationship
- description: Rename object relationship author to author_obj
url: /v1/query
status: 200
response:
message: success
query:
type: rename_relationship
args:
table: article_view
name: author
new_name: author_obj
#Select Query
- description: Nested select on article with renamed object relation
url: /v1/query
status: 200
response:
- id: 1
title: Article 1
content: Sample article content 1
author_obj:
id: 1
name: AUTHOR 1
- id: 2
title: Article 2
content: Sample article content 2
author_obj:
id: 1
name: AUTHOR 1
- id: 3
title: Article 3
content: Sample article content 3
author_obj:
id: 2
name: AUTHOR 2
query:
type: select
args:
table: article_view
columns:
- id
- title
- content
- name: author_obj
columns:
- id
- name
#Drop object relationship
- description: Drop object relationship
url: /v1/query
@ -62,4 +112,4 @@
type: drop_relationship
args:
table: article_view
relationship: author
relationship: author_obj

View File

@ -4,39 +4,12 @@ args:
- type: run_sql
args:
sql: |
drop view article_view
- type: run_sql
args:
sql: |
drop table article
- type: run_sql
args:
sql: |
drop view author_view
- type: run_sql
args:
sql: |
drop table author
- type: run_sql
args:
sql: |
drop view hge_tests.address_view
- type: run_sql
args:
sql: |
drop table hge_tests.address
- type: run_sql
args:
sql: |
drop view hge_tests.resident_view
- type: run_sql
args:
sql: |
drop table hge_tests.resident
DROP VIEW article_view;
DROP TABLE article;
DROP VIEW author_view;
DROP TABLE author;
DROP VIEW hge_tests.address_view;
DROP TABLE hge_tests.address;
DROP VIEW hge_tests.resident_view;
DROP TABLE hge_tests.resident;
cascade: true

View File

@ -9,15 +9,65 @@ args:
id serial primary key,
name text unique
);
insert into author (name) values ('Author 1'), ('Author 2');
create table article(
id serial primary key,
title text not null,
content text not null,
author_id integer not null references author(id)
);
insert into article (title, content, author_id) values
('article 1 by author 1', 'content for article 1', 1),
('article 2 by author 1', 'content for article 2', 1),
('article 1 by author 2', 'content for article 3', 2);
- type: track_table
args:
schema: public
name: author
- type: track_table
args:
schema: public
name: article
#Insert Author table data
- type: insert
#Object relationship
- type: create_object_relationship
args:
table: article
name: author
using:
foreign_key_constraint_on: author_id
#Array relationship
- type: create_array_relationship
args:
table: author
objects:
- name: Author 1
- name: Author 2
name: articles
using:
foreign_key_constraint_on:
table: article
column: author_id
#Article select permission for user
- type: create_select_permission
args:
table: article
role: user
permission:
columns:
- id
- title
- content
- author_id
filter:
author:
id: X-Hasura-User-Id
#Article insert permission for user
- type: create_insert_permission
args:
table: article
role: user
permission:
check:
author_id: X-Hasura-User-Id

View File

@ -0,0 +1,70 @@
#Rename columns
- url: /v1/query
status: 200
query:
type: run_sql
args:
sql: |
alter table author rename column id to author_id;
#Select Queries
- url: /v1/query
status: 200
response:
- author_id: 1
name: Author 1
- author_id: 2
name: Author 2
query:
type: select
args:
table: author
columns:
- author_id
- name
- url: /v1/query
status: 200
headers:
X-Hasura-Role: user
X-Hasura-User-Id: '1'
response:
- id: 1
title: article 1 by author 1
author_id: 1
- id: 2
title: article 2 by author 1
author_id: 1
query:
type: select
args:
table: article
columns:
- id
- title
- author_id
#Revert changes
- url: /v1/query
status: 200
query:
type: run_sql
args:
sql: |
alter table author rename column author_id to id;
#Queries post revert
- url: /v1/query
status: 200
response:
- id: 1
name: Author 1
- id: 2
name: Author 2
query:
type: select
args:
table: author
columns:
- id
- name

View File

@ -0,0 +1,55 @@
#Rename article table
- url: /v1/query
status: 200
query:
type: run_sql
args:
sql: |
alter table article rename to articles;
#Perform select
- url: /v1/query
status: 200
response:
- id: 1
title: article 1 by author 1
content: content for article 1
- id: 2
title: article 2 by author 1
content: content for article 2
- id: 3
title: article 1 by author 2
content: content for article 3
query:
type: select
args:
table: articles
columns:
- id
- title
- content
#Revert changes
- url: /v1/query
status: 200
query:
type: run_sql
args:
sql: |
alter table articles rename to article;
#Error
- url: /v1/query
status: 400
response:
path: "$.args.table"
error: table "articles" does not exist
code: not-exists
query:
type: select
args:
table: articles
columns:
- id
- title
- content

View File

@ -0,0 +1,57 @@
#Rename article table and id columns
- url: /v1/query
status: 200
query:
type: run_sql
args:
sql: |
alter table article rename to articles;
alter table articles rename column id to article_id;
#Perform select
- url: /v1/query
status: 200
response:
- article_id: 1
title: article 1 by author 1
content: content for article 1
- article_id: 2
title: article 2 by author 1
content: content for article 2
- article_id: 3
title: article 1 by author 2
content: content for article 3
query:
type: select
args:
table: articles
columns:
- article_id
- title
- content
#Revert changes
- url: /v1/query
status: 200
query:
type: run_sql
args:
sql: |
alter table articles rename to article;
alter table article rename column article_id to id;
#Error
- url: /v1/query
status: 400
response:
path: "$.args.table"
error: table "articles" does not exist
code: not-exists
query:
type: select
args:
table: articles
columns:
- id
- title
- content

View File

@ -3,4 +3,6 @@ args:
- type: run_sql
args:
sql: |
drop table author
drop table article;
drop table author;
cascade: true

View File

@ -471,6 +471,15 @@ class TestRunSQL(DefaultTestQueries):
def test_sql_query_as_user_error(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_query_as_user_error.yaml')
def test_sql_rename_table(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_rename_table.yaml')
def test_sql_rename_columns(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_rename_columns.yaml')
def test_sql_rename_table_and_column(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_rename_table_and_column.yaml')
@classmethod
def dir(cls):
return "queries/v1/run_sql"