Added support for a multi-level partitioned table. Fixes #2554.

This commit is contained in:
Akshay Joshi 2020-01-23 18:49:15 +05:30
parent f5d46bf9f1
commit 198063f046
20 changed files with 605 additions and 76 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features
************
| `Issue #2554 <https://redmine.postgresql.org/issues/2554>`_ - Added support for a multi-level partitioned table.
| `Issue #3452 <https://redmine.postgresql.org/issues/3452>`_ - Added a Schema Diff tool to compare two schemas and generate a diff script.
Housekeeping

View File

@ -26,6 +26,8 @@ Use the fields in the *General* tab to identify the table:
drop-down listbox in the *Schema* field.
* Use the drop-down listbox in the *Tablespace* field to specify the tablespace
in which the table will be stored.
* Move the *Partitioned Table?* switch to the *Yes* in case you want to create a
partitioned table. Option is available for PostgreSQL 10 and above.
* Store notes about the table in the *Comment* field.
Click the *Columns* tab to continue.
@ -449,6 +451,21 @@ icon (+) to add each partition:
* If partition type is Hash then *Modulus* and *Remainder* fields will be
enabled.
Users can create a partition and define them as a partitioned table. Click
the *Edit* icon to expand the properties of a partition.
Use the *Partition* tab to create that partition as a partitioned table.
* Move the *Partitioned Table?* switch to the *Yes* in case you want to create a
partitioned table.
* Select a partition type from the *Partition Type* selection box.
* Use the *Partition Keys* panel to define the partition keys.
View of multi level Partitioned Table in browser tree:
.. image:: images/table_partition_tree.png
:alt: Table dialog partition tree
:align: center
Click the *Parameter* tab to continue.
.. image:: images/table_parameter.png

View File

@ -19,5 +19,12 @@ class BasePartitionTable:
def get_icon_css_class(self, table_info, default_val='icon-table'):
if self.is_table_partitioned(table_info):
return 'icon-partition'
return 'icon-partition_table'
return default_val
def get_partition_icon_css_class(self, table_info,
default_val='icon-partition'):
if 'is_sub_partitioned' in table_info and \
table_info['is_sub_partitioned']:
return 'icon-sub_partition_table'
return default_val

View File

@ -153,6 +153,24 @@ class PartitionsModule(CollectionNodeModule):
"""
return False
@property
def csssnippets(self):
"""
Returns a snippet of css to include in the page
"""
snippets = [
render_template(
"partitions/css/partition.css",
node_type=self.node_type,
_=gettext
)
]
for submodule in self.submodules:
snippets.extend(submodule.csssnippets)
return snippets
blueprint = PartitionsModule(__name__)
@ -194,7 +212,7 @@ class PartitionsView(BaseTableView, DataTypeReader, VacuumSettings,
operations = dict({
'obj': [
{'get': 'properties', 'delete': 'delete', 'put': 'update'},
{'get': 'list', 'post': 'create'}
{'get': 'list', 'post': 'create', 'delete': 'delete'}
],
'delete': [{'delete': 'delete'}, {'delete': 'delete'}],
'nodes': [{'get': 'nodes'}, {'get': 'nodes'}],
@ -287,11 +305,12 @@ class PartitionsView(BaseTableView, DataTypeReader, VacuumSettings,
return internal_server_error(errormsg=rset)
def browser_node(row):
icon = self.get_partition_icon_css_class(row)
return self.blueprint.generate_browser_node(
row['oid'],
tid,
row['name'],
icon=self.get_icon_css_class({}),
icon=icon,
tigger_count=row['triggercount'],
has_enable_triggers=row['has_enable_triggers'],
is_partitioned=row['is_partitioned'],

View File

@ -1 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#def4fd;}.cls-2{fill:#717f8e;}.cls-3,.cls-5,.cls-6{fill:none;stroke-width:0.75px;}.cls-3{stroke:#717f8e;stroke-linecap:round;}.cls-3,.cls-5{stroke-linejoin:round;}.cls-4{fill:#2195e7;}.cls-5{stroke:#c1cbd5;}.cls-6{stroke:#2195e7;stroke-miterlimit:10;}</style></defs><title>coll-partition</title><g id="_2" data-name="2"><path class="cls-1" d="M3.5,13.13a.63.63,0,0,1-.62-.62V11.13l8.25,0V12.5a.63.63,0,0,1-.62.63Z"/><path class="cls-2" d="M3.25,11.5l7.5,0v1a.25.25,0,0,1-.25.25h-7a.25.25,0,0,1-.25-.25v-1m-.75-.75V12.5a1,1,0,0,0,1,1h7a1,1,0,0,0,1-1V10.78l-9,0Z"/><path class="cls-1" d="M2.88,7.13V3.5a.63.63,0,0,1,.63-.62h7a.63.63,0,0,1,.63.63V7.13Z"/><path class="cls-2" d="M10.5,3.25a.25.25,0,0,1,.25.25V6.75H3.25V3.5a.25.25,0,0,1,.25-.25h7m0-.75h-7a1,1,0,0,0-1,1v4h9v-4a1,1,0,0,0-1-1Z"/><polygon class="cls-1" points="2.88 7.13 2.88 5.13 11.13 5.16 11.13 7.13 2.88 7.13"/><path class="cls-2" d="M3.25,5.5l7.5,0V6.75H3.25V5.5M2.5,4.75V7.5h9V4.78l-9,0Z"/><polygon class="cls-1" points="2.88 11.13 2.88 9.13 11.13 9.15 11.13 11.13 2.88 11.13"/><path class="cls-2" d="M3.25,9.5l7.5,0v1.22H3.25V9.5M2.5,8.75V11.5h9V8.78l-9,0Z"/><line class="cls-3" x1="7" y1="2.89" x2="7" y2="7.05"/><line class="cls-3" x1="7" y1="9.58" x2="7" y2="13.05"/><polygon class="cls-1" points="4.88 11.13 4.88 9.13 13.13 9.15 13.13 11.13 4.88 11.13"/><path class="cls-4" d="M5.25,9.5l7.5,0v1.22H5.25V9.5M4.5,8.75V11.5h9V8.78l-9,0Z"/><path class="cls-1" d="M4.5,7.5v-4a1,1,0,0,1,1-1h7a1,1,0,0,1,1,1v4Z"/><line class="cls-5" x1="9" y1="5.5" x2="9" y2="7"/><path class="cls-4" d="M5.25,5.5l7.5,0V6.75H5.25V5.5M4.5,4.75V7.5h9V4.78l-9,0Z"/><line class="cls-6" x1="9" y1="2.5" x2="9" y2="5.5"/><path class="cls-4" d="M12.5,3.25a.25.25,0,0,1,.25.25V4.75H5.25V3.5a.25.25,0,0,1,.25-.25h7m0-.75h-7a1,1,0,0,0-1,1v2h9v-2a1,1,0,0,0-1-1Z"/><path class="cls-1" d="M12.5,13.5h-7a1,1,0,0,1-1-1v-1h9v1A1,1,0,0,1,12.5,13.5Z"/><line class="cls-5" x1="9" y1="11.5" x2="9" y2="12.97"/><line class="cls-5" x1="9" y1="9.5" x2="9" y2="11.97"/><path class="cls-4" d="M5.25,11.5l7.5,0v1a.25.25,0,0,1-.25.25h-7a.25.25,0,0,1-.25-.25v-1m-.75-.75V12.5a1,1,0,0,0,1,1h7a1,1,0,0,0,1-1V10.78l-9,0Z"/></g></svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F2F2F2;}
.st1{fill:none;stroke:#C1CBD5;stroke-width:0.75;stroke-linejoin:round;}
.st2{fill:#2195E7;}
</style>
<title>partition</title>
<g>
<g id="_2_1_">
<path class="st0" d="M0.5,7V1c0-0.6,0.4-1,1-1h9c0.6,0,1,0.4,1,1v6H0.5z"/>
<line class="st1" x1="6" y1="4" x2="6" y2="6.5"/>
<g>
<path class="st2" d="M10.5,0H6.4H5.6H1.5c-0.6,0-1,0.4-1,1v2.2l0,0V4v3h11V4V3.3V1C11.5,0.4,11.1,0,10.5,0z M10.5,0.8
c0.1,0,0.2,0.1,0.2,0.2v2.2H6.4V0.8H10.5z M1.2,1c0-0.1,0.1-0.2,0.2-0.2h4.1v2.5H1.2V1z M10.8,6.2H1.2V4h4.4h0.8h4.4
C10.8,4,10.8,6.2,10.8,6.2z"/>
</g>
</g>
<g id="_2">
<path class="st0" d="M2.5,9V3c0-0.6,0.4-1,1-1h9c0.6,0,1,0.4,1,1v6H2.5z"/>
<line class="st1" x1="8" y1="6" x2="8" y2="8.5"/>
<path class="st0" d="M12.5,14h-9c-0.6,0-1-0.4-1-1v-2h11v2C13.5,13.6,13.1,14,12.5,14z"/>
<line class="st1" x1="8" y1="10.5" x2="8" y2="13.5"/>
<g>
<path class="st2" d="M12.5,2H8.4H7.6H3.5c-0.6,0-1,0.4-1,1v2.2l0,0V6v3h11V6V5.3V3C13.5,2.4,13.1,2,12.5,2z M12.5,2.8
c0.1,0,0.2,0.1,0.2,0.2v2.2H8.4V2.8H12.5z M3.2,3c0-0.1,0.1-0.2,0.2-0.2h4.1v2.5H3.2V3z M12.8,8.2H3.2V6h4.4h0.8h4.4
C12.8,6,12.8,8.2,12.8,8.2z"/>
<path class="st2" d="M2.5,13c0,0.6,0.4,1,1,1h9c0.6,0,1-0.4,1-1v-2.7h-11V13z M3.2,11h9.5v2c0,0.1-0.1,0.2-0.2,0.2h-9
c-0.1,0-0.2-0.1-0.2-0.2L3.2,11L3.2,11z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F2F2F2;}
.st1{fill:#C1CBD5;}
.st2{fill:#2195E7;}
.st3{fill:#F2F2F2;stroke:#34495E;stroke-width:0.5;stroke-linejoin:bevel;stroke-miterlimit:10;}
.st4{fill:#34495E;}
</style>
<title>inherits</title>
<g>
<path class="st0" d="M3.5,3h9c0.6,0,1,0.4,1,1v8c0,0.6-0.4,1-1,1h-9c-0.6,0-1-0.4-1-1V4C2.5,3.4,2.9,3,3.5,3z"/>
<polygon class="st1" points="13,9.1 8.4,9.1 8.4,6 7.6,6 7.6,9.1 3,9.1 3,9.9 7.6,9.9 7.6,13 8.4,13 8.4,9.9 13,9.9 "/>
<path class="st2" d="M12.5,3h-9c-0.6,0-1,0.4-1,1v8c0,0.6,0.4,1,1,1h9c0.6,0,1-0.4,1-1V4C13.5,3.4,13.1,3,12.5,3z M12.5,3.8
c0.1,0,0.2,0.1,0.2,0.2v2.1H8.4V3.8H12.5z M3.5,3.8h4.1v2.4H3.2V4C3.2,3.9,3.4,3.8,3.5,3.8z M12.5,12.2h-9c-0.1,0-0.2-0.1-0.2-0.2
V6.9h9.5V12C12.8,12.1,12.6,12.2,12.5,12.2z"/>
<path class="st3" d="M4.1,7.6c-2,0-3.6-1.6-3.6-3.5c0-2,1.6-3.5,3.6-3.5c1.9,0,3.5,1.5,3.5,3.5C7.6,6,6,7.6,4.1,7.6L4.1,7.6z"/>
<path class="st4" d="M4.5,2.2H3.7v0H2.9v4h0.8V4.8h0.8c0.7,0,1.3-0.6,1.3-1.3S5.2,2.2,4.5,2.2z M4.5,4.1H3.7V2.9h0.9
c0.3,0,0.6,0.3,0.6,0.6S4.9,4.1,4.5,4.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F2F2F2;}
.st1{fill:none;stroke:#C1CBD5;stroke-width:0.75;stroke-linejoin:round;}
.st2{fill:#2195E7;}
.st3{fill:#F2F2F2;stroke:#34495E;stroke-width:0.5;stroke-linejoin:bevel;stroke-miterlimit:10;}
.st4{fill:#34495E;}
</style>
<title>partition</title>
<g>
<path class="st0" d="M2.5,10.1v-6c0-0.6,0.4-1,1-1h9c0.6,0,1,0.4,1,1v6H2.5z"/>
<line class="st1" x1="8" y1="7.1" x2="8" y2="9.6"/>
<path class="st2" d="M12.5,3.1H8.4H7.6H3.5c-0.6,0-1,0.4-1,1v2.2l0,0v0.8v3h11v-3V6.4V4.1C13.5,3.5,13.1,3.1,12.5,3.1z M12.5,3.9
c0.1,0,0.2,0.1,0.2,0.2v2.2H8.4V3.9H12.5z M3.2,4.1c0-0.1,0.1-0.2,0.2-0.2h4.1v2.5H3.2V4.1z M12.8,9.3H3.2V7.1h4.4h0.8h4.4
C12.8,7.1,12.8,9.3,12.8,9.3z"/>
<g>
<path class="st0" d="M12.5,14.5h-9c-0.6,0-1-0.4-1-1v-2h11v2C13.5,14.1,13.1,14.5,12.5,14.5z"/>
<line class="st1" x1="8" y1="11" x2="8" y2="14"/>
<path class="st2" d="M2.5,13.5c0,0.6,0.4,1,1,1h9c0.6,0,1-0.4,1-1v-2.7h-11V13.5z M3.2,11.5h9.5v2c0,0.1-0.1,0.2-0.2,0.2h-9
c-0.1,0-0.2-0.1-0.2-0.2L3.2,11.5L3.2,11.5z"/>
</g>
<path class="st3" d="M5,8.5C3,8.5,1.4,6.9,1.4,5C1.4,3,3,1.5,5,1.5C6.9,1.5,8.5,3,8.5,5C8.5,6.9,6.9,8.5,5,8.5L5,8.5z"/>
<path class="st4" d="M5.4,3.1H4.6l0,0H3.8v4h0.8V5.7h0.8c0.7,0,1.3-0.6,1.3-1.3S6.1,3.1,5.4,3.1z M5.4,5H4.6V3.8h0.9
c0.3,0,0.6,0.3,0.6,0.6S5.8,5,5.4,5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -562,7 +562,9 @@ function(
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; }));
@ -861,7 +863,7 @@ function(
editable: true, type: 'collection',
group: 'partition', mode: ['edit', 'create'],
deps: ['is_partitioned', 'partition_type'],
canEdit: false, canDelete: true,
canEdit: true, canDelete: true,
customDeleteTitle: gettext('Detach Partition'),
customDeleteMsg: gettext('Are you sure you wish to detach this partition?'),
columns:['is_attach', 'partition_name', 'is_default', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'],

View File

@ -0,0 +1,36 @@
.icon-coll-partition {
background-image: url('{{ url_for('NODE-partition.static', filename='img/coll-partition.svg') }}') !important;
background-repeat: no-repeat;
background-size: 20px !important;
align-content: center;
vertical-align: middle;
height: 1.3em;
}
.icon-partition {
background-image: url('{{ url_for('NODE-partition.static', filename='img/partition.svg') }}') !important;
background-repeat: no-repeat;
background-size: 20px !important;
align-content: center;
vertical-align: middle;
height: 1.3em;
}
.icon-partition_table {
background-image: url('{{ url_for('NODE-partition.static', filename='img/partition_table.svg') }}') !important;
background-repeat: no-repeat;
background-size: 20px !important;
align-content: center;
vertical-align: middle;
height: 1.3em;
}
.icon-sub_partition_table {
background-image: url('{{ url_for('NODE-partition.static', filename='img/sub_partition_table.svg') }}') !important;
background-repeat: no-repeat;
background-size: 20px !important;
align-content: center;
vertical-align: middle;
border-radius: 10px;
height: 1.3em;
}

View File

@ -248,10 +248,13 @@ define('pgadmin.node.table_partition_utils', [
values_in: undefined,
values_modulus: undefined,
values_remainder: undefined,
is_sub_partitioned: false,
sub_partition_type: 'range',
},
keys:['partition_name'],
schema: [{
id: 'oid', label: gettext('OID'), type: 'text',
mode: ['properties'],
},{
id: 'is_attach', label:gettext('Operation'), cell: 'switch', type: 'switch',
options: {'onText': gettext('Attach'), 'offText': gettext('Create'), 'width': 65},
@ -261,6 +264,11 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
},
disabled: function(m) {
if (m instanceof Backbone.Model && m.isNew() && !m.top.isNew())
return false;
return true;
},
},{
id: 'partition_name', label: gettext('Name'), type: 'text', cell:'string',
cellHeaderClasses: 'width_percent_15',
@ -269,6 +277,11 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
}, cellFunction: getPartitionCell,
disabled: function(m) {
if (m instanceof Backbone.Model && m.isNew())
return false;
return true;
},
},{
id: 'is_default', label: gettext('Default'), type: 'switch', cell:'switch',
cellHeaderClasses: 'width_percent_5', min_version: 110000,
@ -283,6 +296,16 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
},
disabled: 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 false;
return true;
},
},{
id: 'values_from', label: gettext('From'), type:'text',
cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
@ -295,6 +318,14 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
},
disabled: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'range' &&
m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return false;
return true;
},
},{
id: 'values_to', label: gettext('To'), type:'text',
cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
@ -307,6 +338,14 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
},
disabled: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'range' &&
m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return false;
return true;
},
},{
id: 'values_in', label: gettext('In'), type:'text',
cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
@ -319,6 +358,14 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
},
disabled: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'list' &&
m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return false;
return true;
},
},{
id: 'values_modulus', label: gettext('Modulus'), type:'int',
cell:Backgrid.Extension.StringDepCell,
@ -331,6 +378,14 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
},
disabled: 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 false;
return true;
},
},{
id: 'values_remainder', label: gettext('Remainder'), type:'int',
cell:Backgrid.Extension.StringDepCell,
@ -343,6 +398,131 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
},
disabled: 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 false;
return true;
},
},{
id: 'is_sub_partitioned', label:gettext('Partitioned table?'), cell: 'switch',
group: 'Partition', type: 'switch', mode: ['properties', 'create', 'edit'],
deps: ['is_attach'],
disabled: function(m) {
if(!m.isNew())
return true;
if (m.get('is_attach')) {
setTimeout( function() {
m.set('is_sub_partitioned', false);
}, 10);
return true;
}
return false;
},
},{
id: 'sub_partition_type', label:gettext('Partition Type'),
editable: false, type: 'select2', select2: {allowClear: false},
group: 'Partition', deps: ['is_sub_partitioned'],
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;
},
visible: function(m) {
if (m.isNew())
return true;
return false;
},
disabled: function(m) {
if (!m.isNew() || !m.get('is_sub_partitioned'))
return true;
return false;
},
},{
id: 'sub_partition_keys', label:gettext('Partition Keys'),
model: Backform.PartitionKeyModel,
subnode: Backform.PartitionKeyModel,
editable: true, type: 'collection',
group: 'Partition', mode: ['properties', 'create', 'edit'],
deps: ['is_sub_partitioned', 'sub_partition_type'],
canEdit: false, canDelete: true,
control: 'sub-node-collection',
canAdd: function(m) {
if (m.isNew() && m.get('is_sub_partitioned'))
return true;
return false;
},
canAddRow: function(m) {
var columns = m.top.get('columns'),
typename = m.top.get('typname'),
columns_exist= false;
var max_row_count = 1000;
if (m.get('sub_partition_type') && m.get('sub_partition_type') == 'list')
max_row_count = 1;
/* If columns are not specified by the user then it may be
* possible that he/she selected 'OF TYPE', so we should check
* for that as well.
*/
if (columns.length <= 0 && !_.isUndefined(typename)
&& !_.isNull(typename) && m.of_types_tables.length > 0){
_.each(m.of_types_tables, function(data) {
if (data.label == typename && data.oftype_columns.length > 0){
columns_exist = true;
}
});
} else if (columns.length > 0) {
columns_exist = _.some(columns.pluck('name'));
}
return (m.get('sub_partition_keys') &&
m.get('sub_partition_keys').length < max_row_count && columns_exist
);
},
disabled: function(m) {
if (m.get('sub_partition_keys') && m.get('sub_partition_keys').models.length > 0) {
setTimeout(function () {
var coll = m.get('sub_partition_keys');
coll.remove(coll.filter(function() { return true; }));
}, 10);
}
},
visible: function(m) {
if (m.isNew())
return true;
return false;
},
},{
id: 'sub_partition_scheme', label: gettext('Partition Scheme'),
type: 'note', group: 'Partition', mode: ['edit'],
visible: function(m) {
if (!m.isNew() && !_.isUndefined(m.get('sub_partition_scheme')) &&
m.get('sub_partition_scheme') != '')
return true;
return false;
},
disabled: function(m) {
if (!m.isNew()) {
this.text = m.get('sub_partition_scheme');
}
},
}],
validate: function() {
var partition_name = this.get('partition_name'),
@ -352,11 +532,14 @@ define('pgadmin.node.table_partition_utils', [
values_in = this.get('values_in'),
values_modulus = this.get('values_modulus'),
values_remainder = this.get('values_remainder'),
is_sub_partitioned = this.get('is_sub_partitioned'),
sub_partition_keys = this.get('sub_partition_keys'),
msg;
// Have to clear existing validation before initiating current state
// validation only
this.errorModel.clear();
this.top.errorModel.clear();
if (_.isUndefined(partition_name) || _.isNull(partition_name) ||
String(partition_name).replace(/^\s+|\s+$/g, '') === '') {
@ -365,6 +548,13 @@ define('pgadmin.node.table_partition_utils', [
return msg;
}
if (is_sub_partitioned && this.isNew() &&
!_.isNull(sub_partition_keys) && sub_partition_keys.length <= 0) {
msg = gettext('Please specify at least one key for partitioned table.');
this.top.errorModel.set('sub_partition_keys', msg);
return msg;
}
if (this.top.get('partition_type') === 'range') {
if (is_default !== true && (_.isUndefined(values_from) ||
_.isNull(values_from) || String(values_from).replace(/^\s+|\s+$/g, '') === '')) {

View File

@ -1004,7 +1004,7 @@ define('pgadmin.node.table', [
editable: true, type: 'collection',
group: 'partition', mode: ['edit', 'create'],
deps: ['is_partitioned', 'partition_type', 'typname'],
canEdit: false, canDelete: true,
canEdit: true, canDelete: true,
customDeleteTitle: gettext('Detach Partition'),
customDeleteMsg: gettext('Are you sure you wish to detach this partition?'),
columns:['is_attach', 'partition_name', 'is_default', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'],

View File

@ -5,7 +5,9 @@ SELECT rel.oid, rel.relname AS name,
rel.relnamespace AS schema_id,
nsp.nspname AS schema_name,
(CASE WHEN rel.relkind = 'p' THEN true ELSE false END) AS is_partitioned,
(CASE WHEN rel.relkind = 'p' THEN pg_get_partkeydef(rel.oid::oid) ELSE '' END) AS partition_scheme
(CASE WHEN rel.relkind = 'p' THEN true ELSE false END) AS is_sub_partitioned,
(CASE WHEN rel.relkind = 'p' THEN pg_get_partkeydef(rel.oid::oid) ELSE '' END) AS partition_scheme,
(CASE WHEN rel.relkind = 'p' THEN pg_get_partkeydef(rel.oid::oid) ELSE '' END) AS sub_partition_scheme
FROM
(SELECT * FROM pg_inherits WHERE inhparent = {{ tid }}::oid) inh
LEFT JOIN pg_class rel ON inh.inhrelid = rel.oid

View File

@ -5,7 +5,9 @@ SELECT rel.oid, rel.relname AS name,
rel.relnamespace AS schema_id,
nsp.nspname AS schema_name,
(CASE WHEN rel.relkind = 'p' THEN true ELSE false END) AS is_partitioned,
(CASE WHEN rel.relkind = 'p' THEN pg_get_partkeydef(rel.oid::oid) ELSE '' END) AS partition_scheme
(CASE WHEN rel.relkind = 'p' THEN true ELSE false END) AS is_sub_partitioned,
(CASE WHEN rel.relkind = 'p' THEN pg_get_partkeydef(rel.oid::oid) ELSE '' END) AS partition_scheme,
(CASE WHEN rel.relkind = 'p' THEN pg_get_partkeydef(rel.oid::oid) ELSE '' END) AS sub_partition_scheme
FROM
(SELECT * FROM pg_inherits WHERE inhparent = {{ tid }}::oid) inh
LEFT JOIN pg_class rel ON inh.inhrelid = rel.oid

View File

@ -67,13 +67,13 @@ class TestBasePartitionTable(BaseTestGenerator):
('#get_icon_css_class when table is partitioned '
'it returns icon-partition class',
'it returns icon-partition_table class',
dict(
test='get_icon_css_class',
input_parameters=dict(
is_partitioned=True
),
expected_return='icon-partition'
expected_return='icon-partition_table'
)),
('#get_icon_css_class when table is not partitioned '
'it returns icon-table class',

View File

@ -44,6 +44,15 @@ class TableAddTestCase(BaseTestGenerator):
'PPAS/PG 10.0 and below.'
)
),
('Create Multilevel Range partitioned table with subpartition table',
dict(url='/browser/table/obj/',
server_min_version=100000,
partition_type='range',
multilevel_partition=True,
skip_msg='Partitioned table are not supported by '
'PPAS/PG 10.0 and below.'
)
),
('Create List partitioned table with 2 partitions',
dict(url='/browser/table/obj/',
server_min_version=100000,
@ -52,6 +61,15 @@ class TableAddTestCase(BaseTestGenerator):
'PPAS/PG 10.0 and below.'
)
),
('Create Multilevel List partitioned table with subpartition table',
dict(url='/browser/table/obj/',
server_min_version=100000,
partition_type='list',
multilevel_partition=True,
skip_msg='Partitioned table are not supported by '
'PPAS/PG 10.0 and below.'
)
),
('Create Hash partitioned table with 2 partitions',
dict(url='/browser/table/obj/',
server_min_version=110000,
@ -205,63 +223,20 @@ class TableAddTestCase(BaseTestGenerator):
data['partition_type'] = self.partition_type
data['is_partitioned'] = True
if self.partition_type == 'range':
data['partitions'] = \
[{'values_from': "'2010-01-01'",
'values_to': "'2010-12-31'",
'is_attach': False,
'partition_name': 'emp_2010'
},
{'values_from': "'2011-01-01'",
'values_to': "'2011-12-31'",
'is_attach': False,
'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'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}]
tables_utils.get_range_partitions_data(data, 'Default')
elif hasattr(self, 'multilevel_partition'):
tables_utils.get_range_partitions_data(
data, None, True)
else:
tables_utils.get_range_partitions_data(data)
elif self.partition_type == 'list':
data['partitions'] = \
[{'values_in': "'2012-01-01', '2012-12-31'",
'is_attach': False,
'partition_name': 'emp_2012'
},
{'values_in': "'2013-01-01', '2013-12-31'",
'is_attach': False,
'partition_name': 'emp_2013'
}]
data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}]
if hasattr(self, 'multilevel_partition'):
tables_utils.get_list_partitions_data(data, True)
else:
tables_utils.get_list_partitions_data(data)
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'}]
tables_utils.get_hash_partitions_data(data)
# Add table
response = self.tester.post(

View File

@ -33,6 +33,14 @@ class TableUpdateTestCase(BaseTestGenerator):
mode='create'
)
),
('Create partitions with partition table of existing range '
'partitioned table',
dict(url='/browser/table/obj/',
server_min_version=100000,
partition_type='range',
mode='multilevel'
)
),
('Create partitions of existing list partitioned table',
dict(url='/browser/table/obj/',
server_min_version=100000,
@ -40,6 +48,14 @@ class TableUpdateTestCase(BaseTestGenerator):
mode='create'
)
),
('Create partitions with partition table of existing list '
'partitioned table',
dict(url='/browser/table/obj/',
server_min_version=100000,
partition_type='list',
mode='multilevel'
)
),
('Detach partition from existing range partitioned table',
dict(url='/browser/table/obj/',
server_min_version=100000,

View File

@ -190,6 +190,28 @@ def set_partition_data(server, db_name, schema_name, table_name,
}]
}
)
if partition_type == 'range' and mode == 'multilevel':
data['partitions'].update(
{'added': [{'values_from': "'2014-01-01'",
'values_to': "'2014-12-31'",
'is_attach': False,
'partition_name': 'sale_2014_sub_part',
'is_sub_partitioned': True,
'sub_partition_type': 'range',
'sub_partition_keys':
[{'key_type': 'column', 'pt_column': 'sales'}]
},
{'values_from': "'2015-01-01'",
'values_to': "'2015-12-31'",
'is_attach': False,
'partition_name': 'sale_2015_sub_part',
'is_sub_partitioned': True,
'sub_partition_type': 'list',
'sub_partition_keys':
[{'key_type': 'column', 'pt_column': 'sales'}]
}]
}
)
elif partition_type == 'list' and mode == 'create':
data['partitions'].update(
{'added': [{'values_in': "'2016-01-01', '2016-12-31'",
@ -201,6 +223,26 @@ def set_partition_data(server, db_name, schema_name, table_name,
}]
}
)
elif partition_type == 'list' and mode == 'multilevel':
data['partitions'].update(
{'added': [{'values_in': "'2016-01-01', '2016-12-31'",
'is_attach': False,
'partition_name': 'sale_2016_sub_part',
'is_sub_partitioned': True,
'sub_partition_type': 'list',
'sub_partition_keys':
[{'key_type': 'column', 'pt_column': 'sales'}]
},
{'values_in': "'2017-01-01', '2017-12-31'",
'is_attach': False,
'partition_name': 'sale_2017_sub_part',
'is_sub_partitioned': True,
'sub_partition_type': 'list',
'sub_partition_keys':
[{'key_type': 'column', 'pt_column': 'sales'}]
}]
}
)
elif partition_type == 'range' and mode == 'detach':
partition_id = create_table_for_partition(server, db_name, schema_name,
table_name, 'partition',
@ -320,3 +362,124 @@ def get_table_common_data():
"name": "autovacuum_freeze_table_age"
}]
}
def get_range_partitions_data(data, mode=None, multilevel_partition=False):
"""
This function returns the partitions data for range partition.
:param data:
:param mode:
:param multilevel_partition:
:return:
"""
data['partitions'] = \
[{'values_from': "'2010-01-01'",
'values_to': "'2010-12-31'",
'is_attach': False,
'partition_name': 'emp_2010'
},
{'values_from': "'2011-01-01'",
'values_to': "'2011-12-31'",
'is_attach': False,
'partition_name': 'emp_2011'
}]
if mode == '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'
}]
if multilevel_partition:
data['partitions'] = \
[{'values_from': "'2010-01-01'",
'values_to': "'2010-12-31'",
'is_attach': False,
'partition_name': 'emp_2010_multi_level',
'is_sub_partitioned': True,
'sub_partition_type': 'range',
'sub_partition_keys':
[{'key_type': 'column', 'pt_column': 'empno'}]
},
{'values_from': "'2011-01-01'",
'values_to': "'2011-12-31'",
'is_attach': False,
'partition_name': 'emp_2011_multi_level',
'is_sub_partitioned': True,
'sub_partition_type': 'list',
'sub_partition_keys':
[{'key_type': 'column', 'pt_column': 'empno'}]
}]
data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}]
def get_list_partitions_data(data, multilevel_partition=False):
"""
This function returns the partitions data for list partition.
:param data:
:param multilevel_partition:
:return:
"""
data['partitions'] = \
[{'values_in': "'2012-01-01', '2012-12-31'",
'is_attach': False,
'partition_name': 'emp_2012'
},
{'values_in': "'2013-01-01', '2013-12-31'",
'is_attach': False,
'partition_name': 'emp_2013'
}]
if multilevel_partition:
data['partitions'] = \
[{'values_in': "'2012-01-01', '2012-12-31'",
'is_attach': False,
'partition_name': 'emp_2012_multi_level',
'is_sub_partitioned': True,
'sub_partition_type': 'list',
'sub_partition_keys':
[{'key_type': 'column', 'pt_column': 'empno'}]
},
{'values_in': "'2013-01-01', '2013-12-31'",
'is_attach': False,
'partition_name': 'emp_2013_multi_level',
'is_sub_partitioned': True,
'sub_partition_type': 'range',
'sub_partition_keys':
[{'key_type': 'column', 'pt_column': 'empno'}]
}]
data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}]
def get_hash_partitions_data(data):
"""
This function returns the partitions data for hash partition.
:param data:
:return:
"""
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'}]

View File

@ -676,17 +676,19 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
def get_partition_scheme(self, data):
partition_scheme = None
if 'partition_type' in data \
and data['partition_type'] == 'range':
part_type = 'sub_partition_type' if 'sub_partition_type' in data \
else 'partition_type'
part_keys = 'sub_partition_keys' if 'sub_partition_keys' in data \
else 'partition_keys'
if part_type in data and data[part_type] == 'range':
partition_scheme = 'RANGE ('
elif 'partition_type' in data \
and data['partition_type'] == 'list':
elif part_type in data and data[part_type] == 'list':
partition_scheme = 'LIST ('
elif 'partition_type' in data \
and data['partition_type'] == 'hash':
elif part_type in data and data[part_type] == 'hash':
partition_scheme = 'HASH ('
for row in data['partition_keys']:
for row in data[part_keys]:
if row['key_type'] == 'column':
partition_scheme += self.qtIdent(
self.conn, row['pt_column']) + ', '
@ -694,7 +696,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
partition_scheme += row['expression'] + ', '
# Remove extra space and comma
if len(data['partition_keys']) > 0:
if len(data[part_keys]) > 0:
partition_scheme = partition_scheme[:-2]
partition_scheme += ')'
@ -1235,7 +1237,9 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
'partition_name': partition_name,
'values_from': range_from,
'values_to': range_to,
'is_default': is_default
'is_default': is_default,
'is_sub_partitioned': row['is_sub_partitioned'],
'sub_partition_scheme': row['sub_partition_scheme']
})
elif data['partition_type'] == 'list':
if row['partition_value'] == 'DEFAULT':
@ -1251,7 +1255,9 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
'oid': row['oid'],
'partition_name': partition_name,
'values_in': range_in,
'is_default': is_default
'is_default': is_default,
'is_sub_partitioned': row['is_sub_partitioned'],
'sub_partition_scheme': row['sub_partition_scheme']
})
else:
range_part = row['partition_value'].split(
@ -1265,7 +1271,9 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
'oid': row['oid'],
'partition_name': partition_name,
'values_modulus': range_modulus,
'values_remainder': range_remainder
'values_remainder': range_remainder,
'is_sub_partitioned': row['is_sub_partitioned'],
'sub_partition_scheme': row['sub_partition_scheme']
})
data['partitions'] = partitions
@ -1339,6 +1347,11 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
+ ', REMAINDER ' +\
remainder_str + ')'
# Check if partition is again declare as partitioned table.
if 'is_sub_partitioned' in row and row['is_sub_partitioned']:
part_data['partition_scheme'] = self.get_partition_scheme(row)
part_data['is_partitioned'] = True
if 'is_attach' in row and row['is_attach']:
partition_sql = render_template(
"/".join([self.partition_template_path, 'attach.sql']),