mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
parent
00227728cb
commit
6c20ca8a55
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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'] }],
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
||||
|
@ -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 => {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
405
console/src/components/Services/Data/TableModify/ColumnEditor.js
Normal file
405
console/src/components/Services/Data/TableModify/ColumnEditor.js
Normal 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>
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={onDeleteFK}
|
||||
data-test="remove-constraint-button"
|
||||
>
|
||||
{' '}
|
||||
Remove Constraint{' '}
|
||||
</Button>{' '}
|
||||
|
||||
</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;
|
@ -336,7 +336,6 @@ hr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chevron_mar_right {
|
||||
margin-right: 5px;
|
||||
}
|
@ -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,
|
||||
|
@ -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>
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={onDeleteFK}
|
||||
data-test="remove-constraint-button"
|
||||
>
|
||||
{' '}
|
||||
Remove Constraint{' '}
|
||||
</Button>{' '}
|
||||
|
||||
</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,
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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>
|
||||
|
||||
<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;
|
@ -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>
|
||||
|
||||
{lcol.join(',')}
|
||||
→
|
||||
{rTable} :: {rcol.join(',')}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
|
||||
{finalRTable} :: {rcol.join(',')}
|
||||
→
|
||||
{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>
|
||||
|
||||
<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,
|
||||
|
@ -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>
|
||||
|
||||
{getGrayText(lcol)}
|
||||
→
|
||||
{rTable} :: {rcol}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
|
||||
{rTable} :: {rcol}
|
||||
→
|
||||
{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>
|
||||
|
||||
<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,
|
||||
});
|
||||
|
@ -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>
|
||||
|
||||
{lcol.join(',')}
|
||||
→
|
||||
{rTable} :: {rcol.join(',')}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
|
||||
{finalRTable} :: {rcol.join(',')}
|
||||
→
|
||||
{lcol.join(',')}
|
||||
</span>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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
276
server/src-exec/Migrate.hs
Normal 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
|
@ -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
|
||||
|
@ -1,4 +1,3 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
module Hasura.RQL.DDL.Metadata
|
||||
( TableMeta
|
||||
|
||||
|
@ -38,6 +38,7 @@ module Hasura.RQL.DDL.Permission
|
||||
, addPermP1
|
||||
, addPermP2
|
||||
|
||||
, dropView
|
||||
, DropPerm
|
||||
, runDropPerm
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 ()
|
||||
|
42
server/src-lib/Hasura/RQL/DDL/Relationship/Rename.hs
Normal file
42
server/src-lib/Hasura/RQL/DDL/Relationship/Rename.hs
Normal 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
|
130
server/src-lib/Hasura/RQL/DDL/Relationship/Types.hs
Normal file
130
server/src-lib/Hasura/RQL/DDL/Relationship/Types.hs
Normal 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)
|
@ -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
|
||||
|
381
server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs
Normal file
381
server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
8
server/src-rsr/migrate_from_9_to_10.sql
Normal file
8
server/src-rsr/migrate_from_9_to_10.sql
Normal 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;
|
@ -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
|
||||
|
15
server/test/testcases/alter_table.yaml
Normal file
15
server/test/testcases/alter_table.yaml
Normal 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"
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
70
server/tests-py/queries/v1/run_sql/sql_rename_columns.yaml
Normal file
70
server/tests-py/queries/v1/run_sql/sql_rename_columns.yaml
Normal 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
|
55
server/tests-py/queries/v1/run_sql/sql_rename_table.yaml
Normal file
55
server/tests-py/queries/v1/run_sql/sql_rename_table.yaml
Normal 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
|
@ -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
|
@ -3,4 +3,6 @@ args:
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
drop table author
|
||||
drop table article;
|
||||
drop table author;
|
||||
cascade: true
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user