Add support for primary key, foreign key, unique key, indexes and triggers on partitioned tables for PG/EPAS 11. Fixes #3412

This commit is contained in:
Khushboo Vashi
2018-07-27 17:31:21 +05:30
committed by Akshay Joshi
parent 0138dee989
commit 0cb25bde63
14 changed files with 200 additions and 49 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

View File

@@ -13,6 +13,7 @@ Features
| `Feature #2214 <https://redmine.postgresql.org/issues/2214>`_ - Add support for SCRAM password changes (requires psycopg2 >= 2.8).
| `Feature #3074 <https://redmine.postgresql.org/issues/3074>`_ - Add support for reset saved password.
| `Feature #3397 <https://redmine.postgresql.org/issues/3397>`_ - Add support for Trigger and JIT stats in the graphical query plan viewer.
| `Feature #3412 <https://redmine.postgresql.org/issues/3412>`_ - Add support for primary key, foreign key, unique key, indexes and triggers on partitioned tables for PG/EPAS 11.
| `Feature #3506 <https://redmine.postgresql.org/issues/3506>`_ - Allow the user to specify a fixed port number in the runtime to aid cookie whitelisting etc.
| `Feature #3510 <https://redmine.postgresql.org/issues/3510>`_ - Add a menu option to the runtime to copy the appserver URL to the clipboard.

View File

@@ -256,6 +256,31 @@ Use the fields in the **Like** box to specify which attributes of an existing ta
* Move the *With storage?* switch to the *Yes* position to copy storage settings.
* Move the *With comments?* switch to the *Yes* position to copy comments.
With PostgreSQL 10 forward, the *Partition* tab will be visible.
Click the *Partition* tab to continue.
.. image:: images/table_partition.png
:alt: Table dialog partition tab
Use the fields in the *partition* tab to create the partitions for the table:
* Select a partition type from the *Partition Type* selection box. There are 3 options available; Range, List and Hash. Hash option will only enable for PostgreSQL version >= 11.
Use the *Partition Keys* panel to define the partition keys. Click the *Add* icon (+) to add each partition keys selection:
* Select a partition key type in the *Keytype* field.
* Select a partition column in the *Column* field if Column option selected for *Keytype* field .
* Specify the expression in the *Expression* field if Expression option selected for the *Keytype* field.
Use the *Partitions* panel to define the partitions of a table. Click the *Add* icon (+) to add each partition:
* Move the *Operation* switch to *attach* to attach the partition, by default it is *create*.
* Use the *Name* field to add the name of the partition.
* If partition type is Range then *From* and *To* fields will be enabled.
* If partition type is List then *In* field will be enabled.
* If partition type is Hash then *Modulus* and *Remainder* fields will be enabled.
Click the *Parameter* tab to continue.
.. image:: images/table_parameter.png

View File

@@ -205,7 +205,8 @@ define('pgadmin.node.column', [
m.top.get('primary_key').length > 0 &&
!_.isUndefined(m.top.get('primary_key').first().get('oid'))
) || (
m.top.has('is_partitioned') && m.top.get('is_partitioned')
m.top.has('is_partitioned') && m.top.get('is_partitioned') &&
m.top.node_info.server && m.top.node_info.server.version < 11000
))
) {
return true;
@@ -235,7 +236,8 @@ define('pgadmin.node.column', [
// If table is partitioned table then disable
if (m.top && !_.isUndefined(m.top.get('is_partitioned')) &&
m.top.get('is_partitioned'))
m.top.get('is_partitioned') && m.top.node_info.server &&
m.top.node_info.server.version < 11000)
{
setTimeout(function () {
m.set('is_primary_key', false);

View File

@@ -1061,13 +1061,15 @@ define('pgadmin.node.foreign_key', [
var t = pgBrowser.tree, i = item, d = itemData, parents = [],
immediate_parent_table_found = false,
is_immediate_parent_table_partitioned = false;
is_immediate_parent_table_partitioned = false,
s_version = this.getTreeNodeHierarchy(i).server.version;
// To iterate over tree to check parent node
while (i) {
// If table is partitioned table then return false
if (!immediate_parent_table_found && (d._type == 'table' || d._type == 'partition')) {
immediate_parent_table_found = true;
if ('is_partitioned' in d && d.is_partitioned) {
if ('is_partitioned' in d && d.is_partitioned && s_version < 110000) {
is_immediate_parent_table_partitioned = true;
}
}

View File

@@ -44,14 +44,15 @@ define('pgadmin.node.primary_key', [
var t = pgBrowser.tree, i = item, d = itemData, parents = [],
immediate_parent_table_found = false,
is_immediate_parent_table_partitioned = false;
is_immediate_parent_table_partitioned = false,
s_version = this.getTreeNodeHierarchy(i).server.version;
// To iterate over tree to check parent node
while (i) {
// If table is partitioned table then return false
if (!immediate_parent_table_found && (d._type == 'table' || d._type == 'partition')) {
immediate_parent_table_found = true;
if ('is_partitioned' in d && d.is_partitioned) {
if ('is_partitioned' in d && d.is_partitioned && s_version < 110000) {
is_immediate_parent_table_partitioned = true;
}
}

View File

@@ -44,14 +44,15 @@ define('pgadmin.node.unique_constraint', [
var t = pgBrowser.tree, i = item, d = itemData, parents = [],
immediate_parent_table_found = false,
is_immediate_parent_table_partitioned = false;
is_immediate_parent_table_partitioned = false,
s_version = this.getTreeNodeHierarchy(i).server.version;
// To iterate over tree to check parent node
while (i) {
// If table is partitioned table then return false
if (!immediate_parent_table_found && (d._type == 'table' || d._type == 'partition')) {
immediate_parent_table_found = true;
if ('is_partitioned' in d && d.is_partitioned) {
if ('is_partitioned' in d && d.is_partitioned && s_version < 110000) {
is_immediate_parent_table_partitioned = true;
}
}

View File

@@ -75,8 +75,9 @@ class IndexesModule(CollectionNodeModule):
if super(IndexesModule, self).BackendSupported(manager, **kwargs):
conn = manager.connection(did=kwargs['did'])
# In case of partitioned table return false.
if 'tid' in kwargs and manager.version >= 100000:
# If PG version > 100000 and < 110000 then index is
# not supported for partitioned table.
if 'tid' in kwargs and 100000 <= manager.version < 110000:
return not backend_supported(self, manager, **kwargs)
if 'vid' not in kwargs:

View File

@@ -560,14 +560,15 @@ define('pgadmin.node.index', [
var t = pgBrowser.tree, i = item, d = itemData, parents = [],
immediate_parent_table_found = false,
is_immediate_parent_table_partitioned = false;
is_immediate_parent_table_partitioned = false,
s_version = this.getTreeNodeHierarchy(i).server.version;
// To iterate over tree to check parent node
while (i) {
// Do not allow creating index on partitioned tables.
if (!immediate_parent_table_found &&
_.indexOf(['table', 'partition'], d._type) > -1) {
immediate_parent_table_found = true;
if ('is_partitioned' in d && d.is_partitioned) {
if ('is_partitioned' in d && d.is_partitioned && s_version < 110000) {
is_immediate_parent_table_partitioned = true;
}
}

View File

@@ -237,6 +237,8 @@ define('pgadmin.node.table_partition_utils', [
values_from: undefined,
values_to: undefined,
values_in: undefined,
values_modulus: undefined,
values_remainder: undefined,
},
keys:['partition_name'],
schema: [{
@@ -252,7 +254,7 @@ define('pgadmin.node.table_partition_utils', [
},
},{
id: 'partition_name', label: gettext('Name'), type: 'text', cell:'string',
cellHeaderClasses: 'width_percent_25',
cellHeaderClasses: 'width_percent_15',
editable: function(m) {
if (m instanceof Backbone.Model && m.isNew())
return true;
@@ -261,11 +263,11 @@ define('pgadmin.node.table_partition_utils', [
},{
id: 'values_from', label: gettext('From'), type:'text',
cell:Backgrid.Extension.StringDepCell,
cellHeaderClasses: 'width_percent_20',
cellHeaderClasses: 'width_percent_15',
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type == 'range' &&
m.handler.top.attributes.partition_type === 'range' &&
m instanceof Backbone.Model && m.isNew())
return true;
return false;
@@ -273,11 +275,11 @@ define('pgadmin.node.table_partition_utils', [
},{
id: 'values_to', label: gettext('To'), type:'text',
cell:Backgrid.Extension.StringDepCell,
cellHeaderClasses: 'width_percent_20',
cellHeaderClasses: 'width_percent_15',
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type == 'range' &&
m.handler.top.attributes.partition_type === 'range' &&
m instanceof Backbone.Model && m.isNew())
return true;
return false;
@@ -285,11 +287,35 @@ define('pgadmin.node.table_partition_utils', [
},{
id: 'values_in', label: gettext('In'), type:'text',
cell:Backgrid.Extension.StringDepCell,
cellHeaderClasses: 'width_percent_25',
cellHeaderClasses: 'width_percent_15',
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type == 'list' &&
m.handler.top.attributes.partition_type === 'list' &&
m instanceof Backbone.Model && m.isNew())
return true;
return false;
},
},{
id: 'values_modulus', label: gettext('Modulus'), type:'int',
cell:Backgrid.Extension.StringDepCell,
cellHeaderClasses: 'width_percent_15',
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'hash' &&
m instanceof Backbone.Model && m.isNew())
return true;
return false;
},
},{
id: 'values_remainder', label: gettext('Remainder'), type:'int',
cell:Backgrid.Extension.StringDepCell,
cellHeaderClasses: 'width_percent_15 width_percent_20',
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'hash' &&
m instanceof Backbone.Model && m.isNew())
return true;
return false;
@@ -300,6 +326,8 @@ define('pgadmin.node.table_partition_utils', [
values_from = this.get('values_from'),
values_to = this.get('values_to'),
values_in = this.get('values_in'),
values_modulus = this.get('values_modulus'),
values_remainder = this.get('values_remainder'),
msg;
// Have to clear existing validation before initiating current state
@@ -307,31 +335,43 @@ define('pgadmin.node.table_partition_utils', [
this.errorModel.clear();
if (_.isUndefined(partition_name) || _.isNull(partition_name) ||
String(partition_name).replace(/^\s+|\s+$/g, '') == '') {
String(partition_name).replace(/^\s+|\s+$/g, '') === '') {
msg = gettext('Partition name cannot be empty.');
this.errorModel.set('partition_name', msg);
return msg;
}
if (this.top.get('partition_type') == 'range') {
if (this.top.get('partition_type') === 'range') {
if (_.isUndefined(values_from) || _.isNull(values_from) ||
String(values_from).replace(/^\s+|\s+$/g, '') == '') {
String(values_from).replace(/^\s+|\s+$/g, '') === '') {
msg = gettext('For range partition From field cannot be empty.');
this.errorModel.set('values_from', msg);
return msg;
} else if (_.isUndefined(values_to) || _.isNull(values_to) ||
String(values_to).replace(/^\s+|\s+$/g, '') == '') {
String(values_to).replace(/^\s+|\s+$/g, '') === '') {
msg = gettext('For range partition To field cannot be empty.');
this.errorModel.set('values_to', msg);
return msg;
}
} else if (this.top.get('partition_type') == 'list') {
} else if (this.top.get('partition_type') === 'list') {
if (_.isUndefined(values_in) || _.isNull(values_in) ||
String(values_in).replace(/^\s+|\s+$/g, '') == '') {
String(values_in).replace(/^\s+|\s+$/g, '') === '') {
msg = gettext('For list partition In field cannot be empty.');
this.errorModel.set('values_in', msg);
return msg;
}
} else if (this.top.get('partition_type') === 'hash') {
if (_.isUndefined(values_modulus) || _.isNull(values_modulus) ||
String(values_modulus).replace(/^\s+|\s+$/g, '') === '') {
msg = gettext('For hash partition Modulus field cannot be empty.');
this.errorModel.set('values_modulus', msg);
return msg;
} else if (_.isUndefined(values_remainder) || _.isNull(values_remainder) ||
String(values_remainder).replace(/^\s+|\s+$/g, '') === '') {
msg = gettext('For hash partition Remainder field cannot be empty.');
this.errorModel.set('values_remainder', msg);
return msg;
}
}
return null;

View File

@@ -593,7 +593,9 @@ define('pgadmin.node.table', [
control: 'unique-col-collection',
columns : ['name', 'columns'],
canAdd: function(m) {
if (m.get('is_partitioned')) {
if (m.get('is_partitioned') && !_.isUndefined(m.top.node_info) && !_.isUndefined(m.top.node_info.server)
&& !_.isUndefined(m.top.node_info.server.version) &&
m.top.node_info.server.version < 110000) {
setTimeout(function() {
var coll = m.get('primary_key');
coll.remove(coll.filter(function() { return true; }));
@@ -620,7 +622,9 @@ define('pgadmin.node.table', [
canEdit: true, canDelete: true, deps:['is_partitioned'],
control: 'unique-col-collection',
canAdd: function(m) {
if (m.get('is_partitioned')) {
if (m.get('is_partitioned') && !_.isUndefined(m.top.node_info) && !_.isUndefined(m.top.node_info.server)
&& !_.isUndefined(m.top.node_info.server.version) &&
m.top.node_info.server.version < 110000) {
setTimeout(function() {
var coll = m.get('foreign_key');
coll.remove(coll.filter(function() { return true; }));
@@ -656,7 +660,8 @@ define('pgadmin.node.table', [
control: 'unique-col-collection',
columns : ['name', 'columns'],
canAdd: function(m) {
if (m.get('is_partitioned')) {
if (m.get('is_partitioned') && !_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
&& !_.isUndefined(m.node_info.server.version) && m.node_info.server.version < 110000) {
setTimeout(function() {
var coll = m.get('unique_constraint');
coll.remove(coll.filter(function() { return true; }));
@@ -834,11 +839,22 @@ define('pgadmin.node.table', [
id: 'partition_type', label:gettext('Partition Type'),
editable: false, type: 'select2', select2: {allowClear: false},
group: 'partition', deps: ['is_partitioned'],
options:[{
label: gettext('Range'), value: 'range',
},{
label: gettext('List'), value: 'list',
}],
options: function() {
var options = [{
label: gettext('Range'), value: 'range',
},{
label: gettext('List'), value: 'list',
}];
if(!_.isUndefined(this.node_info) && !_.isUndefined(this.node_info.server)
&& !_.isUndefined(this.node_info.server.version) &&
this.node_info.server.version >= 110000) {
options.push({
label: gettext('Hash'), value: 'hash',
});
}
return options;
},
mode:['create'],
visible: function(m) {
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
@@ -963,7 +979,7 @@ define('pgadmin.node.table', [
canEdit: false, canDelete: true,
customDeleteTitle: gettext('Detach Partition'),
customDeleteMsg: gettext('Are you sure you wish to detach this partition?'),
columns:['is_attach', 'partition_name', 'values_from', 'values_to', 'values_in'],
columns:['is_attach', 'partition_name', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'],
control: Backform.SubNodeCollectionControl.extend({
row: Backgrid.PartitionRow,
initialize: function() {
@@ -1040,6 +1056,8 @@ define('pgadmin.node.table', [
'</li><li> ',
gettext('In: Enabled for list partition. Values must be comma(,) separated and quoted with single quote.'),
'</li></ul></li></ul>',
gettext('Modulus/Remainder: Enabled for hash partition.'),
'</li></ul></li></ul>',
].join(''),
visible: function(m) {
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)

View File

@@ -36,6 +36,12 @@ class TableAddTestCase(BaseTestGenerator):
server_min_version=100000,
partition_type='list'
)
),
('Create Hash partitioned table with 2 partitions',
dict(url='/browser/table/obj/',
server_min_version=110000,
partition_type='hash'
)
)
]
@@ -93,13 +99,14 @@ class TableAddTestCase(BaseTestGenerator):
"attoptions": [],
"seclabels": []
},
{"name": "DOJ",
"cltype": "date",
"attacl": [],
"is_primary_key": False,
"attoptions": [],
"seclabels": []
}
{
"name": "DOJ",
"cltype": "date",
"attacl": [],
"is_primary_key": False,
"attoptions": [],
"seclabels": []
}
],
"exclude_constraint": [],
"fillfactor": "",
@@ -208,7 +215,9 @@ class TableAddTestCase(BaseTestGenerator):
'is_attach': False,
'partition_name': 'emp_2011'
}]
else:
data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}]
elif self.partition_type == 'list':
data['partitions'] = \
[{'values_in': "'2012-01-01', '2012-12-31'",
'is_attach': False,
@@ -218,8 +227,22 @@ class TableAddTestCase(BaseTestGenerator):
'is_attach': False,
'partition_name': 'emp_2013'
}]
data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}]
data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}]
else:
data['partitions'] = \
[{'values_modulus': "24",
'values_remainder': "3",
'is_attach': False,
'partition_name': 'emp_2016'
},
{'values_modulus': "8",
'values_remainder': "2",
'is_attach': False,
'partition_name': 'emp_2017'
}]
data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'empno'}]
# Add table
response = self.tester.post(

View File

@@ -204,7 +204,9 @@ define('pgadmin.node.trigger', [
disabled: function(m) {
// Disabled if table is a partitioned table.
if (_.has(m, 'node_info') && _.has(m.node_info, 'table') &&
_.has(m.node_info.table, 'is_partitioned') && m.node_info.table.is_partitioned)
_.has(m.node_info.table, 'is_partitioned') &&
m.node_info.table.is_partitioned && m.node_info.server.version < 110000
)
{
setTimeout(function(){
m.set('is_row_trigger', false);

View File

@@ -874,6 +874,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
# Table & Schema declaration so that we can use them in child nodes
schema = data['schema']
table = data['name']
is_partitioned = 'is_partitioned' in data and data['is_partitioned']
data = self._formatter(did, scid, tid, data)
@@ -1139,7 +1140,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
# 5) Reverse engineered sql for PARTITIONS
##########################################
"""
if 'is_partitioned' in data and data['is_partitioned']:
if is_partitioned:
SQL = render_template("/".join([self.partition_template_path,
'nodes.sql']),
scid=scid, tid=tid)
@@ -1211,6 +1212,9 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
elif 'partition_type' in data \
and data['partition_type'] == 'list':
partition_scheme = 'LIST ('
elif 'partition_type' in data \
and data['partition_type'] == 'hash':
partition_scheme = 'HASH ('
for row in data['partition_keys']:
if row['key_type'] == 'column':
@@ -2205,7 +2209,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
'values_from': range_from,
'values_to': range_to
})
else:
elif data['partition_type'] == 'list':
range_part = \
row['partition_value'].split('FOR VALUES IN (')[1]
@@ -2215,6 +2219,20 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
'partition_name': partition_name,
'values_in': range_in
})
else:
range_part = row['partition_value'].split(
'FOR VALUES WITH (')[1].split(",")
range_modulus = range_part[0].strip().strip(
"modulus").strip()
range_remainder = range_part[1].strip().\
strip(" remainder").strip(")").strip()
partitions.append({
'oid': row['oid'],
'partition_name': partition_name,
'values_modulus': range_modulus,
'values_remainder': range_remainder
})
data['partitions'] = partitions
@@ -2256,10 +2274,26 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
part_data['partition_value'] = 'FOR VALUES FROM (' + from_str \
+ ') TO (' + to_str + ')'
else:
elif partitions['partition_type'] == 'list':
range_in = row['values_in'].split(',')
in_str = ', '.join("{0}".format(item) for item in range_in)
part_data['partition_value'] = 'FOR VALUES IN (' + in_str + ')'
part_data['partition_value'] = 'FOR VALUES IN (' + in_str\
+ ')'
else:
range_modulus = row['values_modulus'].split(',')
range_remainder = row['values_remainder'].split(',')
modulus_str = ', '.join("{0}".format(item) for item in
range_modulus)
remainder_str = ', '.join("{0}".format(item) for item in
range_remainder)
part_data['partition_value'] = 'FOR VALUES WITH (MODULUS '\
+ modulus_str \
+ ', REMAINDER ' +\
remainder_str + ')'
if 'is_attach' in row and row['is_attach']:
partition_sql = render_template(