1) Added support for Default Partition. Fixes #3938

2) Ensure that record should be add/edited for root partition table with primary keys. Fixes #4104
This commit is contained in:
Khushboo Vashi 2019-04-11 13:25:24 +05:30 committed by Akshay Joshi
parent 9c3925e448
commit a9d964b5ca
8 changed files with 121 additions and 47 deletions

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 247 KiB

View File

@ -14,5 +14,7 @@ Features
Bug fixes Bug fixes
********* *********
| `Bug #3938 <https://redmine.postgresql.org/issues/3938>`_ - Added support for Default Partition.
| `Bug #4104 <https://redmine.postgresql.org/issues/4104>`_ - Ensure that record should be add/edited for root partition table with primary keys.
| `Bug #4138 <https://redmine.postgresql.org/issues/4138>`_ - Fix an issue where the dropdown becomes misaligned/displaced. | `Bug #4138 <https://redmine.postgresql.org/issues/4138>`_ - Fix an issue where the dropdown becomes misaligned/displaced.
| `Bug #4161 <https://redmine.postgresql.org/issues/4161>`_ - Ensure that parameters of procedures for EPAS server 10 and below should be set/reset properly. | `Bug #4161 <https://redmine.postgresql.org/issues/4161>`_ - Ensure that parameters of procedures for EPAS server 10 and below should be set/reset properly.

View File

@ -442,6 +442,7 @@ icon (+) to add each partition:
* Move the *Operation* switch to *attach* to attach the partition, by default it * Move the *Operation* switch to *attach* to attach the partition, by default it
is *create*. is *create*.
* Use the *Name* field to add the name of the partition. * Use the *Name* field to add the name of the partition.
* If partition type is Range or List then *Default* field will be enabled.
* If partition type is Range then *From* and *To* fields will be enabled. * 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 List then *In* field will be enabled.
* If partition type is Hash then *Modulus* and *Remainder* fields will be * If partition type is Hash then *Modulus* and *Remainder* fields will be
@ -523,4 +524,4 @@ three columns and a primary key constraint on the *category_id* column.
* Click the *Info* button (i) to access online help. * Click the *Info* button (i) to access online help.
* Click the *Save* button to save work. * Click the *Save* button to save work.
* Click the *Cancel* button to exit without saving work. * Click the *Cancel* button to exit without saving work.
* Click the *Reset* button to restore configuration parameters. * Click the *Reset* button to restore configuration parameters.

View File

@ -242,6 +242,7 @@ define('pgadmin.node.table_partition_utils', [
oid: undefined, oid: undefined,
is_attach: false, is_attach: false,
partition_name: undefined, partition_name: undefined,
is_default: undefined,
values_from: undefined, values_from: undefined,
values_to: undefined, values_to: undefined,
values_in: undefined, values_in: undefined,
@ -252,8 +253,8 @@ define('pgadmin.node.table_partition_utils', [
schema: [{ schema: [{
id: 'oid', label: gettext('OID'), type: 'text', id: 'oid', label: gettext('OID'), type: 'text',
},{ },{
id: 'is_attach', label:gettext('Operation'), cell: 'switch', id: 'is_attach', label:gettext('Operation'), cell: 'switch', type: 'switch',
type: 'switch', options: { 'onText': gettext('Attach'), 'offText': gettext('Create')}, options: {'onText': gettext('Attach'), 'offText': gettext('Create'), 'width': 65},
cellHeaderClasses: 'width_percent_5', cellHeaderClasses: 'width_percent_5',
editable: function(m) { editable: function(m) {
if (m instanceof Backbone.Model && m.isNew() && !m.top.isNew()) if (m instanceof Backbone.Model && m.isNew() && !m.top.isNew())
@ -268,39 +269,53 @@ define('pgadmin.node.table_partition_utils', [
return true; return true;
return false; return false;
}, cellFunction: getPartitionCell, }, cellFunction: getPartitionCell,
},{
id: 'is_default', label: gettext('Default'), type: 'switch', cell:'switch',
cellHeaderClasses: 'width_percent_5', min_version: 110000,
options: {'onText': gettext('Yes'), 'offText': gettext('No')},
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 === 'list') &&
m instanceof Backbone.Model && m.isNew() &&
m.handler.top.node_info.server.version >= 110000)
return true;
return false;
},
},{ },{
id: 'values_from', label: gettext('From'), type:'text', id: 'values_from', label: gettext('From'), type:'text',
cell:Backgrid.Extension.StringDepCell, cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
cellHeaderClasses: 'width_percent_15', cellHeaderClasses: 'width_percent_15',
editable: function(m) { editable: function(m) {
if(m.handler && m.handler.top && if(m.handler && m.handler.top &&
m.handler.top.attributes && m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'range' && m.handler.top.attributes.partition_type === 'range' &&
m instanceof Backbone.Model && m.isNew()) m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return true; return true;
return false; return false;
}, },
},{ },{
id: 'values_to', label: gettext('To'), type:'text', id: 'values_to', label: gettext('To'), type:'text',
cell:Backgrid.Extension.StringDepCell, cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
cellHeaderClasses: 'width_percent_15', cellHeaderClasses: 'width_percent_15',
editable: function(m) { editable: function(m) {
if(m.handler && m.handler.top && if(m.handler && m.handler.top &&
m.handler.top.attributes && m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'range' && m.handler.top.attributes.partition_type === 'range' &&
m instanceof Backbone.Model && m.isNew()) m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return true; return true;
return false; return false;
}, },
},{ },{
id: 'values_in', label: gettext('In'), type:'text', id: 'values_in', label: gettext('In'), type:'text',
cell:Backgrid.Extension.StringDepCell, cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
cellHeaderClasses: 'width_percent_15', cellHeaderClasses: 'width_percent_15',
editable: function(m) { editable: function(m) {
if(m.handler && m.handler.top && if(m.handler && m.handler.top &&
m.handler.top.attributes && m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'list' && m.handler.top.attributes.partition_type === 'list' &&
m instanceof Backbone.Model && m.isNew()) m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return true; return true;
return false; return false;
}, },
@ -331,6 +346,7 @@ define('pgadmin.node.table_partition_utils', [
}], }],
validate: function() { validate: function() {
var partition_name = this.get('partition_name'), var partition_name = this.get('partition_name'),
is_default = this.get('is_default'),
values_from = this.get('values_from'), values_from = this.get('values_from'),
values_to = this.get('values_to'), values_to = this.get('values_to'),
values_in = this.get('values_in'), values_in = this.get('values_in'),
@ -350,20 +366,20 @@ define('pgadmin.node.table_partition_utils', [
} }
if (this.top.get('partition_type') === 'range') { if (this.top.get('partition_type') === 'range') {
if (_.isUndefined(values_from) || _.isNull(values_from) || if (is_default !== true && (_.isUndefined(values_from) ||
String(values_from).replace(/^\s+|\s+$/g, '') === '') { _.isNull(values_from) || String(values_from).replace(/^\s+|\s+$/g, '') === '')) {
msg = gettext('For range partition From field cannot be empty.'); msg = gettext('For range partition From field cannot be empty.');
this.errorModel.set('values_from', msg); this.errorModel.set('values_from', msg);
return msg; return msg;
} else if (_.isUndefined(values_to) || _.isNull(values_to) || } else if (is_default !== true && (_.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.'); msg = gettext('For range partition To field cannot be empty.');
this.errorModel.set('values_to', msg); this.errorModel.set('values_to', msg);
return 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) || if (is_default !== true && (_.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.'); msg = gettext('For list partition In field cannot be empty.');
this.errorModel.set('values_in', msg); this.errorModel.set('values_in', msg);
return msg; return msg;

View File

@ -960,17 +960,18 @@ define('pgadmin.node.table', [
id: 'partition_key_note', label: gettext('Partition Keys'), id: 'partition_key_note', label: gettext('Partition Keys'),
type: 'note', group: 'partition', mode: ['create'], type: 'note', group: 'partition', mode: ['create'],
text: [ text: [
'<br>&nbsp;&nbsp;', '<ul><li>',
gettext('Partition table supports two types of keys:'), gettext('Partition table supports two types of keys:'),
'<br><ul><li>',
gettext('Column: User can select any column from the list of available columns.'),
'</li><li>', '</li><li>',
gettext('Expression: User can specify expression to create partition key.'), '<strong>', gettext('Column: '), '</strong>',
'<br><p>', gettext('User can select any column from the list of available columns.'),
gettext('Example'), '</li><li>',
':', '<strong>', gettext('Expression: '), '</strong>',
gettext('User can specify expression to create partition key.'),
'</li><li>',
'<strong>', gettext('Example: '), '</strong>',
gettext('Let\'s say, we want to create a partition table based per year for the column \'saledate\', having datatype \'date/timestamp\', then we need to specify the expression as \'extract(YEAR from saledate)\' as partition key.'), gettext('Let\'s say, we want to create a partition table based per year for the column \'saledate\', having datatype \'date/timestamp\', then we need to specify the expression as \'extract(YEAR from saledate)\' as partition key.'),
'</p></li></ul>', '</li></ul>',
].join(''), ].join(''),
visible: function(m) { visible: function(m) {
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
@ -990,7 +991,7 @@ define('pgadmin.node.table', [
canEdit: false, canDelete: true, canEdit: false, canDelete: true,
customDeleteTitle: gettext('Detach Partition'), customDeleteTitle: gettext('Detach Partition'),
customDeleteMsg: gettext('Are you sure you wish to detach this partition?'), customDeleteMsg: gettext('Are you sure you wish to detach this partition?'),
columns:['is_attach', 'partition_name', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'], columns:['is_attach', 'partition_name', 'is_default', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'],
control: Backform.SubNodeCollectionControl.extend({ control: Backform.SubNodeCollectionControl.extend({
row: Backgrid.PartitionRow, row: Backgrid.PartitionRow,
initialize: function() { initialize: function() {
@ -1053,22 +1054,28 @@ define('pgadmin.node.table', [
id: 'partition_note', label: gettext('Partitions'), id: 'partition_note', label: gettext('Partitions'),
type: 'note', group: 'partition', type: 'note', group: 'partition',
text: [ text: [
'<ul>', '<ul><li>',
' <li>', '<strong>', gettext('Create a table: '), '</strong>',
gettext('Create a table: User can create multiple partitions while creating new partitioned table. Operation switch is disabled in this scenario.'), gettext('User can create multiple partitions while creating new partitioned table. Operation switch is disabled in this scenario.'),
'</li><li>', '</li><li>',
gettext('Edit existing table: User can create/attach/detach multiple partitions. In attach operation user can select table from the list of suitable tables to be attached.'), '<strong>', gettext('Edit existing table: '), '</strong>',
gettext('User can create/attach/detach multiple partitions. In attach operation user can select table from the list of suitable tables to be attached.'),
'</li><li>', '</li><li>',
'<strong>', gettext('Default: '), '</strong>',
gettext('The default partition can store rows that do not fall into any existing partitions range or list.'),
'</li><li>',
'<strong>', gettext('From/To/In input: '), '</strong>',
gettext('From/To/In input: Values for these fields must be quoted with single quote. For more than one partition key values must be comma(,) separated.'), gettext('From/To/In input: Values for these fields must be quoted with single quote. For more than one partition key values must be comma(,) separated.'),
'<br>', '</li><li>',
gettext('Example'), '<strong>', gettext('Example: From/To: '), '</strong>',
':<ul><li>', gettext('Enabled for range partition. Consider partitioned table with multiple keys of type Integer, then values should be specified like \'100\',\'200\'.'),
gettext('From/To: Enabled for range partition. Consider partitioned table with multiple keys of type Integer, then values should be specified like \'100\',\'200\'.'), '</li><li>',
'</li><li> ', '<strong>', gettext('In: '), '</strong>',
gettext('In: Enabled for list partition. Values must be comma(,) separated and quoted with single quote.'), gettext('Enabled for list partition. Values must be comma(,) separated and quoted with single quote.'),
'</li></ul></li></ul>', '</li><li>',
gettext('Modulus/Remainder: Enabled for hash partition.'), '<strong>', gettext('Modulus/Remainder: '), '</strong>',
'</li></ul></li></ul>', gettext('Enabled for hash partition.'),
'</li></ul>',
].join(''), ].join(''),
visible: function(m) { visible: function(m) {
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)

View File

@ -31,6 +31,14 @@ class TableAddTestCase(BaseTestGenerator):
partition_type='range' partition_type='range'
) )
), ),
('Create Range partitioned table with 1 default and 2'
' value based partition',
dict(url='/browser/table/obj/',
server_min_version=110000,
partition_type='range',
is_default=True
)
),
('Create List partitioned table with 2 partitions', ('Create List partitioned table with 2 partitions',
dict(url='/browser/table/obj/', dict(url='/browser/table/obj/',
server_min_version=100000, server_min_version=100000,
@ -215,6 +223,24 @@ class TableAddTestCase(BaseTestGenerator):
'is_attach': False, 'is_attach': False,
'partition_name': 'emp_2011' 'partition_name': 'emp_2011'
}] }]
if hasattr(self, 'is_default'):
data['partitions'] = \
[{'values_from': "'2010-01-01'",
'values_to': "'2010-12-31'",
'is_attach': False,
'partition_name': 'emp_2010_def'
},
{'values_from': "'2011-01-01'",
'values_to': "'2011-12-31'",
'is_attach': False,
'partition_name': 'emp_2011_def'
},
{'values_from': "",
'values_to': "",
'is_attach': False,
'is_default': True,
'partition_name': 'emp_2012_def'
}]
data['partition_keys'] = \ data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}] [{'key_type': 'column', 'pt_column': 'DOJ'}]
elif self.partition_type == 'list': elif self.partition_type == 'list':

View File

@ -2184,16 +2184,23 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
partition_name = row['schema_name'] + '.' + row['name'] partition_name = row['schema_name'] + '.' + row['name']
if data['partition_type'] == 'range': if data['partition_type'] == 'range':
range_part = row['partition_value'].split( if row['partition_value'] == 'DEFAULT':
'FOR VALUES FROM (')[1].split(') TO') is_default = True
range_from = range_part[0] range_from = None
range_to = range_part[1][2:-1] range_to = None
else:
range_part = row['partition_value'].split(
'FOR VALUES FROM (')[1].split(') TO')
range_from = range_part[0]
range_to = range_part[1][2:-1]
is_default = False
partitions.append({ partitions.append({
'oid': row['oid'], 'oid': row['oid'],
'partition_name': partition_name, 'partition_name': partition_name,
'values_from': range_from, 'values_from': range_from,
'values_to': range_to 'values_to': range_to,
'is_default': is_default
}) })
elif data['partition_type'] == 'list': elif data['partition_type'] == 'list':
range_part = \ range_part = \
@ -2251,15 +2258,22 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
part_data['relispartition'] = True part_data['relispartition'] = True
part_data['name'] = row['partition_name'] part_data['name'] = row['partition_name']
if partitions['partition_type'] == 'range': if 'is_default' in row and row['is_default'] and (
partitions['partition_type'] == 'range' or
partitions['partition_type'] == 'list'):
part_data['partition_value'] = 'DEFAULT'
elif partitions['partition_type'] == 'range':
range_from = row['values_from'].split(',') range_from = row['values_from'].split(',')
range_to = row['values_to'].split(',') range_to = row['values_to'].split(',')
from_str = ', '.join("{0}".format(item) for item in range_from) from_str = ', '.join("{0}".format(item) for
to_str = ', '.join("{0}".format(item) for item in range_to) item in range_from)
to_str = ', '.join("{0}".format(item) for
item in range_to)
part_data['partition_value'] = 'FOR VALUES FROM (' + from_str \ part_data['partition_value'] = 'FOR VALUES FROM (' +\
+ ') TO (' + to_str + ')' from_str + ') TO (' +\
to_str + ')'
elif partitions['partition_type'] == 'list': elif partitions['partition_type'] == 'list':
range_in = row['values_in'].split(',') range_in = row['values_in'].split(',')

View File

@ -0,0 +1,8 @@
{# ============= Fetch the primary keys for given object id ============= #}
{% if obj_id %}
SELECT at.attname, ty.typname
FROM pg_attribute at LEFT JOIN pg_type ty ON (ty.oid = at.atttypid)
WHERE attrelid={{obj_id}}::oid AND attnum = ANY (
(SELECT con.conkey FROM pg_class rel LEFT OUTER JOIN pg_constraint con ON con.conrelid=rel.oid
AND con.contype='p' WHERE rel.relkind IN ('r','s','t', 'p') AND rel.oid = {{obj_id}}::oid)::oid[])
{% endif %}