Added support for expression in exclusion constraints. Fixes #5571

This commit is contained in:
Aditya Toshniwal 2020-12-24 12:50:57 +05:30 committed by Akshay Joshi
parent a92595012f
commit 5448de2d3f
19 changed files with 247 additions and 116 deletions

View File

@ -58,13 +58,15 @@ Click the *Columns* tab to continue.
:alt: Exclusion constraint dialog columns tab
:align: center
Use the fields in the *Columns* tab to to specify the column(s) to which the
constraint applies. Use the drop-down listbox next to *Column* to select a
column and click the *Add* icon (+) to provide details of the action on the
column:
Use the fields in the *Columns* tab to specify the column(s) or expression(s)
to which the constraint applies. Use the *Is expression ?* switch to enable
expression text input. Use the drop-down listbox next to *Column*
to select a column. Once the *Column* is selected or the *Expression* is
entered then click the *Add* icon (+) to provide details of the action on the
column/expression:
* The *Column* field is populated with the selection made in the *Column*
drop-down listbox.
* The *Col/Exp* field is populated with the selection made in the *Column*
drop-down listbox or the *Expression* entered.
* If applicable, use the drop-down listbox in the *Operator class* to specify
the operator class that will be used by the index for the column.
* Move the *DESC* switch to *DESC* to specify a descending sort order. The

BIN
docs/en_US/images/exclusion_constraint_columns.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 93 KiB

BIN
docs/en_US/images/exclusion_constraint_sql.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -19,6 +19,7 @@ Housekeeping
Bug fixes
*********
| `Issue #5571 <https://redmine.postgresql.org/issues/5571>`_ - Added support for expression in exclusion constraints.
| `Issue #5875 <https://redmine.postgresql.org/issues/5875>`_ - Ensure that the 'template1' database should not be visible after pg_upgrade.
| `Issue #5965 <https://redmine.postgresql.org/issues/5965>`_ - Ensure that the macro query result should be download properly.
| `Issue #5973 <https://redmine.postgresql.org/issues/5973>`_ - Added appropriate help message and a placeholder for letting users know about the account password expiry for Login/Group Role.

View File

@ -560,17 +560,16 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings,
Returns:
"""
data = request.args if request.args else None
data = request.args
try:
if data and 'col_type' in data:
result = exclusion_utils.get_operator(
self.conn, data['col_type'],
self.blueprint.show_system_objects)
result = exclusion_utils.get_operator(
self.conn, data.get('col_type', None),
self.blueprint.show_system_objects)
return make_json_response(
data=result,
status=200
)
return make_json_response(
data=result,
status=200
)
except Exception as e:
return internal_server_error(errormsg=str(e))

View File

@ -778,58 +778,18 @@ class ExclusionConstraintView(PGChildNodeView):
"""
try:
SQL = render_template(
"/".join([self.template_path, self._PROPERTIES_SQL]),
did=did, tid=tid, conn=self.conn, cid=exid)
status, result = self.conn.execute_dict(SQL)
status, rows = exclusion_utils.get_exclusion_constraints(
self.conn, did, tid, exid, template_path=self.template_path
)
if not status:
return internal_server_error(errormsg=result)
if len(result['rows']) == 0:
return rows
if len(rows) == 0:
return gone(_("Could not find the exclusion constraint."))
data = result['rows'][0]
data = rows[0]
data['schema'] = self.schema
data['table'] = self.table
sql = render_template(
"/".join([self.template_path, 'get_constraint_cols.sql']),
cid=exid,
colcnt=data['col_count'])
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
columns = []
for row in res['rows']:
nulls_order = True if (row['options'] & 2) else False
order = False if row['options'] & 1 else True
columns.append({"column": row['coldef'].strip('"'),
"oper_class": row['opcname'],
"order": order,
"nulls_order": nulls_order,
"operator": row['oprname']
})
data['columns'] = columns
# Add Include details of the index supported for PG-11+
if self.manager.version >= 110000:
sql = render_template(
"/".join(
[self.template_path, 'get_constraint_include.sql']
),
cid=exid)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
data['include'] = [col['colname'] for col in res['rows']]
if data.get('amname', '') == "":
data['amname'] = 'btree'
SQL = render_template(
"/".join([self.template_path, self._CREATE_SQL]), data=data)

View File

@ -19,6 +19,7 @@ define('pgadmin.node.exclusion_constraint', [
var ExclusionConstraintColumnModel = pgBrowser.Node.Model.extend({
defaults: {
column: undefined,
is_exp: false,
oper_class: undefined,
order: false,
nulls_order: false,
@ -32,8 +33,20 @@ define('pgadmin.node.exclusion_constraint', [
return d;
},
schema: [{
id: 'column', label: gettext('Column'), type:'text', editable: false,
id: 'column', label: gettext('Col/Exp'), type:'text', editable: false,
cell:'string',
},{
id: 'is_exp', label: '', type:'boolean', editable: false,
cell: Backgrid.StringCell.extend({
formatter: {
fromRaw: function (rawValue) {
return rawValue ? 'E' : 'C';
},
toRaw: function (val) {
return val;
},
},
}), visible: false,
},{
id: 'oper_class', label: gettext('Operator class'), type:'text',
node: 'table', url: 'get_oper_class', first_empty: true,
@ -175,7 +188,7 @@ define('pgadmin.node.exclusion_constraint', [
self.column.set('options', []);
if (url && !_.isUndefined(col_type) && !_.isNull(col_type) && col_type != '') {
if (url) {
var node = this.column.get('schema_node'),
eventHandler = m.top || m,
node_info = this.column.get('node_info'),
@ -210,9 +223,11 @@ define('pgadmin.node.exclusion_constraint', [
validate: function() {
this.errorModel.clear();
var operator = this.get('operator'),
column_name = this.get('column');
column_name = this.get('column'),
is_exp = this.get('is_exp');
if (_.isUndefined(operator) || _.isNull(operator)) {
var msg = gettext('Please specify operator for column: ') + column_name;
if(is_exp) msg = gettext('Please specify operator for expression: ') + column_name;
this.errorModel.set('operator', msg);
return msg;
}
@ -231,8 +246,15 @@ define('pgadmin.node.exclusion_constraint', [
var self = this,
node = 'exclusion_constraint',
headerSchema = [{
id: 'column', label:'', type:'text',
node: 'column', control: Backform.NodeListByNameControl.extend({
id: 'is_exp', label: gettext('Is expression ?'), type: 'switch',
control: 'switch', controlLabelClassName: 'control-label pg-el-sm-4 pg-el-12',
controlsClassName: 'pgadmin-controls pg-el-sm-6 pg-el-12',
},{
id: 'column', label: gettext('Column'), type:'text',
controlLabelClassName: 'control-label pg-el-sm-4 pg-el-12',
controlsClassName: 'pgadmin-controls pg-el-sm-6 pg-el-12',
node: 'column', deps: ['is_exp'],
control: Backform.NodeListByNameControl.extend({
initialize: function() {
// Here we will decide if we need to call URL
// Or fetch the data from parent columns collection
@ -310,7 +332,8 @@ define('pgadmin.node.exclusion_constraint', [
}
},
template: _.template([
'<div class="<%=Backform.controlsClassName%> <%=extraClasses.join(\' \')%>">',
'<span class="<%=controlLabelClassName%>"><%=label%></span>',
'<div class="<%=controlsClassName%> <%=extraClasses.join(\' \')%>">',
' <select class="pgadmin-node-select form-control" name="<%=name%>" style="width:100%;" value="<%-value%>" <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> >',
' <% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
@ -363,10 +386,21 @@ define('pgadmin.node.exclusion_constraint', [
readonly: function() {
return !_.isUndefined(self.model.get('oid'));
},
disabled: function(m) {
return m.get('is_exp');
},
},{
id: 'exp', label: gettext('Expression'), type: 'text',
editable: true, deps: ['is_exp'],
controlLabelClassName: 'control-label pg-el-sm-4 pg-el-12',
controlsClassName: 'pgadmin-controls pg-el-sm-6 pg-el-12',
disabled: function(m) {
return !m.get('is_exp');
},
}],
headerDefaults = {column: null},
headerDefaults = {is_exp: false, column: null, exp: null},
gridCols = ['column', 'oper_class', 'order', 'nulls_order', 'operator'];
gridCols = ['is_exp', 'column', 'oper_class', 'order', 'nulls_order', 'operator'];
self.headerData = new (Backbone.Model.extend({
defaults: headerDefaults,
@ -396,22 +430,16 @@ define('pgadmin.node.exclusion_constraint', [
},
generateHeader: function(data) {
var isNew = _.isUndefined(this.model.get('oid'));
var header = [
'<div class="subnode-header-form">',
'<div class="subnode-header-form '+ (isNew ? '' : 'd-none') +'">',
' <div>',
' <div class="row">',
' <div class="col-4">',
' <label class="control-label"><%-column_label%></label>',
' </div>',
' <div class="col-4" header="column"></div>',
' </div>',
' <div header="is_exp"></div>',
' <div header="column"></div>',
' <div header="exp"></div>',
' </div>',
'</div>'].join('\n');
_.extend(data, {
column_label: gettext('Column'),
});
var self = this,
headerTmpl = _.template(header),
$header = $(headerTmpl(data)),
@ -514,20 +542,24 @@ define('pgadmin.node.exclusion_constraint', [
}
if (self.control_data.canAdd) {
self.collection.each(function(m) {
if (!inSelected) {
_.each(checkVars, function(v) {
if (!inSelected) {
val = m.get(v);
inSelected = ((
(_.isUndefined(val) || _.isNull(val)) &&
(_.isUndefined(data[v]) || _.isNull(data[v]))
) ||
(val == data[v]));
}
});
}
});
if(data['is_exp']) {
inSelected = false;
} else {
self.collection.each(function(m) {
if (!inSelected) {
_.each(checkVars, function(v) {
if (!inSelected) {
val = m.get(v);
inSelected = ((
(_.isUndefined(val) || _.isNull(val)) &&
(_.isUndefined(data[v]) || _.isNull(data[v]))
) ||
(val == data[v]));
}
});
}
});
}
}
else {
inSelected = true;
@ -538,23 +570,28 @@ define('pgadmin.node.exclusion_constraint', [
addColumns: function(ev) {
ev.preventDefault();
var self = this,
column = self.headerData.get('column');
var self = this;
let newHeaderData = {
is_exp: self.headerData.get('is_exp'),
column: self.headerData.get('is_exp') ? self.headerData.get('exp') : self.headerData.get('column'),
};
if (column && column != '') {
if (newHeaderData.column && newHeaderData.column != '') {
var coll = self.model.get(self.field.get('name')),
m = new (self.field.get('model'))(
self.headerData.toJSON(), {
newHeaderData, {
silent: true, top: self.model.top,
collection: coll, handler: coll,
}),
col_types =self.field.get('col_types') || [];
for(var i=0; i < col_types.length; i++) {
var col_type = col_types[i];
if (col_type['name'] == m.get('column')) {
m.set({'col_type':col_type['type']});
break;
if(!m.get('is_exp')) {
for(var i=0; i < col_types.length; i++) {
var col_type = col_types[i];
if (col_type['name'] == m.get('column')) {
m.set({'col_type':col_type['type']});
break;
}
}
}
@ -786,7 +823,7 @@ define('pgadmin.node.exclusion_constraint', [
!_.isUndefined(m.get('oid'))) || (_.isFunction(m.isNew) && !m.isNew()));
},
},{
id: 'columns', label: gettext('Columns'),
id: 'columns', label: gettext('Columns/Expressions'),
type: 'collection', group: gettext('Columns'),
deps:['amname'], canDelete: true, editable: false,
canAdd: function(m) {

View File

@ -0,0 +1,14 @@
-- Constraint: Exclusion_$%{}[]()&*^!@"'`\/#
-- ALTER TABLE testschema.tableforexclusion DROP CONSTRAINT "Exclusion_$%{}[]()&*^!@""'`\/#";
ALTER TABLE testschema.tableforexclusion
ADD CONSTRAINT "Exclusion_$%{}[]()&*^!@""'`\/#" EXCLUDE USING gist (
(col1 + col3) WITH <>,
col2 WITH <>)
WITH (FILLFACTOR=12)
WHERE (col1 > 1)
DEFERRABLE INITIALLY DEFERRED;
COMMENT ON CONSTRAINT "Exclusion_$%{}[]()&*^!@""'`\/#" ON testschema.tableforexclusion
IS 'Comment for create';

View File

@ -14,6 +14,9 @@
}, {
"name": "col2",
"cltype": "text"
}, {
"name": "col3",
"cltype": "integer",
}],
"is_partitioned": false,
"schema": "testschema",
@ -76,6 +79,43 @@
"data": {
"name": "Exclusion_$%{}[]()&*^!@\"'`\\/#a"
}
}, {
"type": "create",
"name": "Create Exclusion Constraint with expressions",
"endpoint": "NODE-exclusion_constraint.obj",
"sql_endpoint": "NODE-exclusion_constraint.sql_id",
"data": {
"name": "Exclusion_$%{}[]()&*^!@\"'`\\/#_1",
"comment": "Comment for create",
"fillfactor": "12",
"amname": "gist",
"columns": [
{
"column": "col2",
"order": false,
"nulls_order": false,
"operator": "<>",
"is_sort_nulls_applicable": false,
"is_exp": false
},
{
"column": "(col1+col3)",
"order": false,
"nulls_order": false,
"operator": "<>",
"is_sort_nulls_applicable": false,
"is_exp": true
}
]
},
"expected_sql_file": "create_exclusion_constraint_exp.sql"
}, {
"type": "delete",
"name": "Drop Exclusion Constraint",
"endpoint": "NODE-exclusion_constraint.delete_id",
"data": {
"name": "Exclusion_$%{}[]()&*^!@\"'`\\/#_1a"
}
}
]
}

View File

@ -0,0 +1,14 @@
-- Constraint: Exclusion_$%{}[]()&*^!@"'`\/#
-- ALTER TABLE testschema.tableforexclusion DROP CONSTRAINT "Exclusion_$%{}[]()&*^!@""'`\/#";
ALTER TABLE testschema.tableforexclusion
ADD CONSTRAINT "Exclusion_$%{}[]()&*^!@""'`\/#" EXCLUDE USING gist (
(col1 + col3) WITH <>,
col2 WITH <>)
WITH (FILLFACTOR=12)
WHERE (col1 > 1)
DEFERRABLE INITIALLY DEFERRED;
COMMENT ON CONSTRAINT "Exclusion_$%{}[]()&*^!@""'`\/#" ON testschema.tableforexclusion
IS 'Comment for create';

View File

@ -14,6 +14,9 @@
}, {
"name": "col2",
"cltype": "text"
}, {
"name": "col3",
"cltype": "integer",
}],
"is_partitioned": false,
"schema": "testschema",
@ -49,7 +52,8 @@
"order": false,
"nulls_order": false,
"operator": "<>",
"is_sort_nulls_applicable": false
"is_sort_nulls_applicable": false,
"is_exp": false
}
]
},
@ -90,7 +94,8 @@
"order": false,
"nulls_order": false,
"operator": "<>",
"is_sort_nulls_applicable": false
"is_sort_nulls_applicable": false,
"is_exp": false
}
]
},
@ -115,6 +120,43 @@
"data": {
"name": "Exclusion_$%{}[]()&*^!@\"'`\\/#_1a"
}
}, {
"type": "create",
"name": "Create Exclusion Constraint with expressions",
"endpoint": "NODE-exclusion_constraint.obj",
"sql_endpoint": "NODE-exclusion_constraint.sql_id",
"data": {
"name": "Exclusion_$%{}[]()&*^!@\"'`\\/#_1",
"comment": "Comment for create",
"fillfactor": "12",
"amname": "gist",
"columns": [
{
"column": "col2",
"order": false,
"nulls_order": false,
"operator": "<>",
"is_sort_nulls_applicable": false,
"is_exp": false
},
{
"column": "(col1+col3)",
"order": false,
"nulls_order": false,
"operator": "<>",
"is_sort_nulls_applicable": false,
"is_exp": true
}
]
},
"expected_sql_file": "create_exclusion_constraint_exp.sql"
}, {
"type": "delete",
"name": "Drop Exclusion Constraint",
"endpoint": "NODE-exclusion_constraint.delete_id",
"data": {
"name": "Exclusion_$%{}[]()&*^!@\"'`\\/#_1a"
}
}
]
}

View File

@ -79,7 +79,8 @@ def _get_columns(res):
"order": order,
"nulls_order": nulls_order,
"operator": row['oprname'],
"col_type": row['datatype']
"col_type": row['datatype'],
"is_exp": row['is_exp']
})
return columns
@ -127,6 +128,9 @@ def get_exclusion_constraints(conn, did, tid, exid=None, template_path=None):
ex['include'] = [col['colname'] for col in res['rows']]
if ex.get('amname', '') == "":
ex['amname'] = 'btree'
return True, result['rows']

View File

@ -1,7 +1,7 @@
ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }}
ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} (
{% for col in data.columns %}{% if loop.index != 1 %},
{% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.include|length > 0 %}
{% endif %}{% if col.is_exp %}{{col.column}}{% else %}{{ conn|qtIdent(col.column)}}{% endif %}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.include|length > 0 %}
INCLUDE ({% for col in data.include %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(col)}}{% endfor %}){% endif %}{% if data.fillfactor %}

View File

@ -10,7 +10,8 @@ SELECT
,
coll.collname,
nspc.nspname as collnspname,
format_type(ty.oid,NULL) AS datatype
format_type(ty.oid,NULL) AS datatype,
CASE WHEN pg_get_indexdef(i.indexrelid, {{loop.index}}, true) = a.attname THEN FALSE ELSE TRUE END AS is_exp
FROM pg_index i
JOIN pg_attribute a ON (a.attrelid = i.indexrelid AND attnum = {{loop.index}})
JOIN pg_type ty ON ty.oid=a.atttypid
@ -19,4 +20,4 @@ LEFT OUTER JOIN pg_constraint c ON (c.conindid = i.indexrelid) LEFT OUTER JOIN p
LEFT OUTER JOIN pg_collation coll ON a.attcollation=coll.oid
LEFT OUTER JOIN pg_namespace nspc ON coll.collnamespace=nspc.oid
WHERE i.indexrelid = {{cid}}::oid
{% endfor %}
{% endfor %}

View File

@ -1,3 +1,4 @@
{% if type is not none %}
SELECT DISTINCT op.oprname as oprname
FROM pg_operator op,
( SELECT oid
@ -27,4 +28,9 @@ FROM pg_operator op,
UNION SELECT 'serial', 0) t1
WHERE typname = {{type|qtLiteral}}) AS types
WHERE oprcom > 0 AND
(op.oprleft=types.oid OR op.oprright=types.oid)
(op.oprleft=types.oid OR op.oprright=types.oid)
{% else %}
SELECT DISTINCT op.oprname as oprname
FROM pg_operator op
WHERE oprcom > 0
{% endif %}

View File

@ -1,7 +1,7 @@
ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }}
ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} (
{% for col in data.columns %}{% if loop.index != 1 %},
{% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
{% endif %}{% if col.is_exp %}{{col.column}}{% else %}{{ conn|qtIdent(col.column)}}{% endif %}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %}

View File

@ -10,7 +10,8 @@ SELECT
,
coll.collname,
nspc.nspname as collnspname,
format_type(ty.oid,NULL) AS col_type
format_type(ty.oid,NULL) AS datatype,
CASE WHEN pg_get_indexdef(i.indexrelid, {{loop.index}}, true) = a.attname THEN FALSE ELSE TRUE END AS is_exp
FROM pg_index i
JOIN pg_attribute a ON (a.attrelid = i.indexrelid AND attnum = {{loop.index}})
JOIN pg_type ty ON ty.oid=a.atttypid

View File

@ -1,3 +1,4 @@
{% if type is not none %}
SELECT DISTINCT op.oprname as oprname
FROM pg_operator op,
( SELECT oid
@ -26,4 +27,9 @@ FROM pg_operator op,
UNION SELECT 'serial', 0) t1
WHERE typname = {{type|qtLiteral}}) AS types
WHERE oprcom > 0 AND
(op.oprleft=types.oid OR op.oprright=types.oid)
(op.oprleft=types.oid OR op.oprright=types.oid)
{% else %}
SELECT DISTINCT op.oprname as oprname
FROM pg_operator op
WHERE oprcom > 0
{% endif %}

View File

@ -259,9 +259,13 @@ define([
*/
_.extend(
Backform.InputControl.prototype, {
defaults: _.extend(Backform.InputControl.prototype.defaults, {
controlLabelClassName: Backform.controlLabelClassName,
controlsClassName: Backform.controlsClassName,
}),
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>" for="<%=cId%>"><%=label%></label>',
'<div class="<%=Backform.controlContainerClassName%>">',
'<label class="<%=controlLabelClassName%>" for="<%=cId%>"><%=label%></label>',
'<div class="<%=controlsClassName%>">',
' <input type="<%=type%>" id="<%=cId%>" class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>" name="<%=name%>" maxlength="<%=maxlength%>" value="<%-value%>" placeholder="<%-placeholder%>" <%=disabled ? "disabled" : ""%> <%=readonly ? "readonly aria-readonly=true" : ""%> <%=required ? "required" : ""%> />',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',