diff --git a/docs/en_US/column_dialog.rst b/docs/en_US/column_dialog.rst index 276ed4e31..3b41c5bf0 100644 --- a/docs/en_US/column_dialog.rst +++ b/docs/en_US/column_dialog.rst @@ -39,9 +39,69 @@ are disabled if inapplicable.) a text value. * Use the drop-down listbox next to *Collation* to apply a collation setting to the column. + +Click the *Constraints* tab to continue. + +.. image:: images/column_constraints.png + :alt: Column dialog constraints tab + :align: center + +Use the fields in the *Constraints* tab to specify constraints for the column. +(Fields are disabled if inapplicable.) + * Use the *Default Value* field to specify a default data value. * Move the *Not Null* switch to the *Yes* position to specify the column may not contain null values. The default is *No*. +* Use the *Type* field to specify the column type (NONE/IDENTITY/GENERATED). + The default is *NONE*. + +Click the *IDENTITY* type to create Identity column. + +.. image:: images/column_constraint_identity.png + :alt: Column dialog constraints tab + :align: center + +Use the following fields to create *IDENTITY* column. Identity columns are +applicable for PG/EPAS version 10 and above. + +* Use the *Identity* field to specify ALWAYS or BY DEFAULT. This clause is + used to determine how the sequence value is given precedence over a + user-specified value in an INSERT statement. +* Use the *Increment* field to specify which value is added to the current + sequence value to create a new value. +* Provide a value in the *Start* field to specify the beginning value of the + sequence. The default starting value is MINVALUE for ascending sequences and + MAXVALUE for descending ones. +* Provide a value in the *Minimum* field to specify the minimum value a sequence + can generate. If this clause is not supplied or NO MINVALUE is specified, + then defaults will be used. The defaults are 1 and -263-1 for ascending and + descending sequences, respectively. +* Provide a value in the *Maximum* field to specify the maximum value for the + sequence. If this clause is not supplied or NO MAXVALUE is specified, then + default values will be used. The defaults are 263-1 and -1 for ascending and + descending sequences, respectively. +* Provide a value in the *Cache* field to specify how many sequence numbers are + to be preallocated and stored in memory for faster access. The minimum value + is 1 (only one value can be generated at a time, i.e., no cache), and this is + also the default. +* Move the *Cycled* switch to the *Yes* position to allow the sequence to wrap + around when the MAXVALUE or the MINVALUE has been reached by an ascending or + descending sequence respectively. If the limit is reached, the next number + generated will be the MINVALUE or MAXVALUE, respectively. The default is *No*. + +Click the *GENERATED* type to create Generated column. + +.. image:: images/column_constraint_generated.png + :alt: Column dialog constraints tab + :align: center + +Use the following fields to create *GENERATED* column. Generated columns are +applicable for PG/EPAS version 12 and above. + +* Use the *Expression* field to specify the generation expression. It can + refer to other columns in the table, but not other generated columns. + Any functions and operators used must be immutable. References to other + tables are not allowed. Click the *Variables* tab to continue. diff --git a/docs/en_US/images/column_constraint_generated.png b/docs/en_US/images/column_constraint_generated.png new file mode 100644 index 000000000..e692510d6 Binary files /dev/null and b/docs/en_US/images/column_constraint_generated.png differ diff --git a/docs/en_US/images/column_constraint_identity.png b/docs/en_US/images/column_constraint_identity.png new file mode 100644 index 000000000..94a89e9ba Binary files /dev/null and b/docs/en_US/images/column_constraint_identity.png differ diff --git a/docs/en_US/images/column_constraints.png b/docs/en_US/images/column_constraints.png new file mode 100644 index 000000000..2e76e0192 Binary files /dev/null and b/docs/en_US/images/column_constraints.png differ diff --git a/docs/en_US/images/column_definition.png b/docs/en_US/images/column_definition.png old mode 100755 new mode 100644 index cc925774d..e72ac61be Binary files a/docs/en_US/images/column_definition.png and b/docs/en_US/images/column_definition.png differ diff --git a/docs/en_US/images/column_general.png b/docs/en_US/images/column_general.png old mode 100755 new mode 100644 index 1ed8bddf0..f24189bb6 Binary files a/docs/en_US/images/column_general.png and b/docs/en_US/images/column_general.png differ diff --git a/docs/en_US/images/column_security.png b/docs/en_US/images/column_security.png old mode 100755 new mode 100644 index cb9c4fc5d..2feb43ea1 Binary files a/docs/en_US/images/column_security.png and b/docs/en_US/images/column_security.png differ diff --git a/docs/en_US/images/column_variables.png b/docs/en_US/images/column_variables.png old mode 100755 new mode 100644 index 56c238d8c..c1cebe013 Binary files a/docs/en_US/images/column_variables.png and b/docs/en_US/images/column_variables.png differ diff --git a/docs/en_US/release_notes_4_12.rst b/docs/en_US/release_notes_4_12.rst index a5a10d278..d48177863 100644 --- a/docs/en_US/release_notes_4_12.rst +++ b/docs/en_US/release_notes_4_12.rst @@ -10,6 +10,7 @@ New features ************ | `Issue #4333 `_ - Add support for planner support functions in PostgreSQL 12+ functions. +| `Issue #4334 `_ - Add support for generated columns in Postgres 12+. Housekeeping ************ @@ -20,4 +21,6 @@ Bug fixes | `Issue #4179 `_ - Fix generation of reverse engineered SQL for tables with Greenplum 5.x. | `Issue #4490 `_ - Fix accessibility issue for checkbox in IE11. +| `Issue #4496 `_ - Ensure columns can be created when they are IDENTITY fields with the CYCLE option enabled. +| `Issue #4497 `_ - Ensure purely numeric comments can be saved on new columns. | `Issue #4508 `_ - Fix accessibility issue for Datetime cell in backgrid. diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/__init__.py index fdf72d73d..28420fa9e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/__init__.py @@ -547,7 +547,13 @@ class ColumnsView(PGChildNodeView, DataTypeReader): ) for k, v in data.items(): - data[k] = json.loads(v, encoding='utf-8', cls=ColParamsJSONDecoder) + # comments should be taken as is because if user enters a + # json comment it is parsed by loads which should not happen + if k in ('description',): + data[k] = v + else: + data[k] = json.loads(v, encoding='utf-8', + cls=ColParamsJSONDecoder) required_args = { 'name': 'Name', diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.js index 3ece03f7c..a045dc782 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.js @@ -24,7 +24,7 @@ define('pgadmin.node.column', [ type: 'coll-column', columns: ['name', 'atttypid', 'description'], canDrop: SchemaChildTreeNode.isTreeItemOfChildOfSchema, - canDropCascade: SchemaChildTreeNode.isTreeItemOfChildOfSchema, + canDropCascade: false, }); } @@ -185,13 +185,15 @@ define('pgadmin.node.column', [ attnotnull: false, attlen: null, attprecision: null, - attidentity: undefined, + attidentity: 'a', seqincrement: undefined, seqstart: undefined, seqmin: undefined, seqmax: undefined, seqcache: undefined, seqcycle: undefined, + colconstype: 'n', + genexpr: undefined, }, initialize: function(attrs) { if (_.size(attrs) !== 0) { @@ -479,67 +481,6 @@ define('pgadmin.node.column', [ } return flag; }, - },{ - id: 'defval', label: gettext('Default'), cell: 'string', - type: 'text', group: gettext('Definition'), deps: ['cltype'], - disabled: function(m) { - if(!m.inSchemaWithModelCheck.apply(this, [m])) { - var type = m.get('cltype'); - return type == 'serial' || type == 'bigserial' - || type == 'smallserial'; - } - }, - },{ - id: 'attnotnull', label: gettext('Not NULL?'), cell: 'switch', - type: 'switch', disabled: 'inSchemaWithColumnCheck', cellHeaderClasses:'width_percent_20', - group: gettext('Definition'), editable: 'editable_check_for_table', - options: { onText: gettext('Yes'), offText: gettext('No'), onColor: 'success', offColor: 'primary' }, - },{ - type: 'nested', control: 'fieldset', label: gettext('Identity'), - group: gettext('Definition'), - schema:[{ - id: 'attidentity', label: gettext('Identity'), control: 'select2', - cell: 'select2', select2: { placeholder: 'Select identity', - allowClear: true, - width: '100%', - }, - min_version: 100000, group: gettext('Identity'), - 'options': [ - {label: gettext('ALWAYS'), value: 'a'}, - {label: gettext('BY DEFAULT'), value: 'd'}, - ], - },{ - id: 'seqincrement', label: gettext('Increment'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Identity'), - min: 1, deps: ['attidentity'], disabled: 'isIdentityColumn', - },{ - id: 'seqstart', label: gettext('Start'), type: 'int', - mode: ['properties', 'create'], group: gettext('Identity'), - disabled: function(m) { - if (!m.isNew()) - return true; - let isIdentity = m.get('attidentity'); - if(!_.isUndefined(isIdentity) && !_.isNull(isIdentity) && !_.isEmpty(isIdentity)) - return false; - return true; - }, deps: ['attidentity'], - },{ - id: 'seqmin', label: gettext('Minimum'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Identity'), - deps: ['attidentity'], disabled: 'isIdentityColumn', - },{ - id: 'seqmax', label: gettext('Maximum'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Identity'), - deps: ['attidentity'], disabled: 'isIdentityColumn', - },{ - id: 'seqcache', label: gettext('Cache'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Identity'), - min: 1, deps: ['attidentity'], disabled: 'isIdentityColumn', - },{ - id: 'seqcycle', label: gettext('Cycled'), type: 'switch', - mode: ['properties', 'create', 'edit'], group: gettext('Identity'), - deps: ['attidentity'], disabled: 'isIdentityColumn', - }], },{ id: 'attstattarget', label: gettext('Statistics'), cell: 'string', type: 'text', disabled: 'inSchemaWithColumnCheck', mode: ['properties', 'edit'], @@ -558,6 +499,149 @@ define('pgadmin.node.column', [ {label: 'EXTERNAL', value: 'e'}, {label: 'EXTENDED', value: 'x'}, ], + },{ + id: 'defval', label: gettext('Default'), cell: 'string', + type: 'text', group: gettext('Constraints'), deps: ['cltype', 'colconstype'], + disabled: function(m) { + var is_disabled = false; + if(!m.inSchemaWithModelCheck.apply(this, [m])) { + var type = m.get('cltype'); + is_disabled = (type == 'serial' || type == 'bigserial' || type == 'smallserial'); + } + + is_disabled = is_disabled || m.get('colconstype') != 'n'; + if (is_disabled && m.isNew()) { + setTimeout(function () { + m.set('defval', undefined); + }, 10); + } + + return is_disabled; + }, + },{ + id: 'attnotnull', label: gettext('Not NULL?'), cell: 'switch', + type: 'switch', cellHeaderClasses:'width_percent_20', + group: gettext('Constraints'), editable: 'editable_check_for_table', + options: { onText: gettext('Yes'), offText: gettext('No'), onColor: 'success', offColor: 'primary' }, + deps: ['colconstype'], + disabled: function(m) { + if (m.get('colconstype') == 'i') { + setTimeout(function () { + m.set('attnotnull', true); + }, 10); + } + return m.inSchemaWithColumnCheck(m); + }, + }, { + id: 'colconstype', + label: gettext('Type'), + cell: 'string', + type: 'radioModern', + controlsClassName: 'pgadmin-controls col-12 col-sm-9', + controlLabelClassName: 'control-label col-sm-3 col-12', + group: gettext('Constraints'), + options: function(m) { + var opt_array = [ + {'label': gettext('NONE'), 'value': 'n'}, + {'label': gettext('IDENTITY'), 'value': 'i'}, + ]; + + if (m.top.node_info && m.top.node_info.server && + m.top.node_info.server.version >= 120000) { + // You can't change the existing column to Generated column. + if (m.isNew()) { + opt_array.push({ + 'label': gettext('GENERATED'), + 'value': 'g', + }); + } else { + opt_array.push({ + 'label': gettext('GENERATED'), + 'value': 'g', + 'disabled': true, + }); + } + } + + return opt_array; + }, + disabled: function(m) { + if (!m.isNew() && m.get('colconstype') == 'g') { + return true; + } + return false; + }, + visible: function(m) { + if (m.top.node_info && m.top.node_info.server && + m.top.node_info.server.version >= 100000) { + return true; + } + return false; + }, + }, { + id: 'attidentity', label: gettext('Identity'), control: 'select2', + cell: 'select2', + select2: {placeholder: 'Select identity', allowClear: false, width: '100%'}, + min_version: 100000, group: gettext('Constraints'), + 'options': [ + {label: gettext('ALWAYS'), value: 'a'}, + {label: gettext('BY DEFAULT'), value: 'd'}, + ], + deps: ['colconstype'], visible: 'isTypeIdentity', + disabled: function(m) { + if (!m.isNew()) { + if (m.get('attidentity') == '' && m.get('colconstype') == 'i') { + setTimeout(function () { + m.set('attidentity', m.get('old_attidentity')); + }, 10); + } + } + return false; + }, + }, { + id: 'seqincrement', label: gettext('Increment'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + min: 1, deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', + visible: 'isTypeIdentity', + },{ + id: 'seqstart', label: gettext('Start'), type: 'int', + mode: ['properties', 'create'], group: gettext('Constraints'), + disabled: function(m) { + if (!m.isNew()) + return true; + let isIdentity = m.get('attidentity'); + if(!_.isUndefined(isIdentity) && !_.isNull(isIdentity) && !_.isEmpty(isIdentity)) + return false; + return true; + }, deps: ['attidentity', 'colconstype'], + visible: 'isTypeIdentity', + },{ + id: 'seqmin', label: gettext('Minimum'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', + visible: 'isTypeIdentity', + },{ + id: 'seqmax', label: gettext('Maximum'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', + visible: 'isTypeIdentity', + },{ + id: 'seqcache', label: gettext('Cache'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + min: 1, deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', + visible: 'isTypeIdentity', + },{ + id: 'seqcycle', label: gettext('Cycled'), type: 'switch', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', + visible: 'isTypeIdentity', + },{ + id: 'genexpr', label: gettext('Expression'), type: 'text', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + min_version: 120000, deps: ['colconstype'], visible: 'isTypeGenerated', + disabled: function(m) { + return !m.isNew(); + }, },{ id: 'is_pk', label: gettext('Primary key?'), type: 'switch', disabled: true, mode: ['properties'], @@ -665,12 +749,23 @@ define('pgadmin.node.column', [ } } + let genexpr = this.get('genexpr'); + if (this.get('colconstype') == 'g' && + (_.isUndefined(genexpr) || _.isNull(genexpr) || genexpr == '')) { + msg = gettext('Expression value cannot be empty.'); + this.errorModel.set('genexpr', msg); + return msg; + } else { + this.errorModel.unset('genexpr'); + } + var minimum = this.get('seqmin'), maximum = this.get('seqmax'), start = this.get('seqstart'); - if (!this.isNew() && (this.get('old_attidentity') == 'a' || this.get('old_attidentity') == 'd') && - (this.get('attidentity') == 'a' || this.get('attidentity') == 'd')) { + if (!this.isNew() && this.get('colconstype') == 'i' && + (this.get('old_attidentity') == 'a' || this.get('old_attidentity') == 'd') && + (this.get('attidentity') == 'a' || this.get('attidentity') == 'd')) { if (_.isUndefined(this.get('seqincrement')) || String(this.get('seqincrement')).replace(/^\s+|\s+$/g, '') == '') { msg = gettext('Increment value cannot be empty.'); @@ -745,6 +840,22 @@ define('pgadmin.node.column', [ return false; return true; }, + // Check whether the column is a identity column + isTypeIdentity: function(m) { + let colconstype = m.get('colconstype'); + if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'i') { + return true; + } + return false; + }, + // Check whether the column is a generated column + isTypeGenerated: function(m) { + let colconstype = m.get('colconstype'); + if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'g') { + return true; + } + return false; + }, // We will check if we are under schema node & in 'create' mode notInSchema: function() { if(this.node_info && 'catalog' in this.node_info) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/test_column_add.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/test_column_add.py index bb92cfb77..756fb510c 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/test_column_add.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/test_column_add.py @@ -25,31 +25,83 @@ from pgadmin.utils import server_utils as server_utils class ColumnAddTestCase(BaseTestGenerator): """This class will add new column under table node.""" scenarios = [ - ('Add column', dict(url='/browser/column/obj/')), - ('Add column with Identity', dict(url='/browser/column/obj/', - server_min_version=100000, - identity_opt={ - 'cltype': 'bigint', - 'attidentity': 'a', - 'seqincrement': 1, - 'seqstart': 1, - 'seqmin': 1, - 'seqmax': 10, - 'seqcache': 1, - 'seqcycle': True - })), - ('Add column with Identity', dict(url='/browser/column/obj/', - server_min_version=100000, - identity_opt={ - 'cltype': 'bigint', - 'attidentity': 'd', - 'seqincrement': 2, - 'seqstart': 2, - 'seqmin': 2, - 'seqmax': 2000, - 'seqcache': 1, - 'seqcycle': True - })) + ('Add column', dict( + url='/browser/column/obj/', + data={ + 'cltype': "\"char\"", + 'attacl': [], + 'is_primary_key': False, + 'attnotnull': False, + 'attlen': None, + 'attprecision': None, + 'attoptions':[], + 'seclabels':[], + })), + ('Add Identity column with Always', dict( + url='/browser/column/obj/', + server_min_version=100000, + skip_msg='Identity column are not supported by EPAS/PG 10.0 ' + 'and below.', + data={ + 'cltype': 'bigint', + 'attacl': [], + 'is_primary_key': False, + 'attnotnull': True, + 'attlen': None, + 'attprecision': None, + 'attoptions': [], + 'seclabels': [], + 'colconstype': 'i', + 'attidentity': 'a', + 'seqincrement': 1, + 'seqstart': 1, + 'seqmin': 1, + 'seqmax': 10, + 'seqcache': 1, + 'seqcycle': True + })), + ('Add Identity column with As Default', dict( + url='/browser/column/obj/', + col_data_type='bigint', + server_min_version=100000, + skip_msg='Identity column are not supported by EPAS/PG 10.0 ' + 'and below.', + data={ + 'cltype': 'bigint', + 'attacl': [], + 'is_primary_key': False, + 'attnotnull': True, + 'attlen': None, + 'attprecision': None, + 'attoptions': [], + 'seclabels': [], + 'colconstype': 'i', + 'attidentity': 'd', + 'seqincrement': 2, + 'seqstart': 2, + 'seqmin': 2, + 'seqmax': 2000, + 'seqcache': 1, + 'seqcycle': True + })), + ('Add Generated column', dict( + url='/browser/column/obj/', + col_data_type='bigint', + server_min_version=120000, + skip_msg='Generated column are not supported by EPAS/PG 12.0 ' + 'and below.', + data={ + 'cltype': 'bigint', + 'attacl': [], + 'is_primary_key': False, + 'attnotnull': True, + 'attlen': None, + 'attprecision': None, + 'attoptions': [], + 'seclabels': [], + 'colconstype': 'g', + 'genexpr': '100 * 100' + })), ] def setUp(self): @@ -64,9 +116,7 @@ class ColumnAddTestCase(BaseTestGenerator): raise Exception("Could not connect to server to add " "a table.") if server_con["data"]["version"] < self.server_min_version: - message = "Identity columns are not supported by " \ - "PPAS/PG 10.0 and below." - self.skipTest(message) + self.skipTest(self.skip_msg) db_con = database_utils.connect_database(self, utils.SERVER_GROUP, self.server_id, self.db_id) @@ -87,26 +137,16 @@ class ColumnAddTestCase(BaseTestGenerator): def runTest(self): """This function will add column under table node.""" self.column_name = "test_column_add_%s" % (str(uuid.uuid4())[1:8]) - data = { - "name": self.column_name, - "cltype": "\"char\"", - "attacl": [], - "is_primary_key": False, - "attnotnull": False, - "attlen": None, - "attprecision": None, - "attoptions": [], - "seclabels": [] - } + self.data.update({ + 'name': self.column_name + }) - if hasattr(self, 'identity_opt'): - data.update(self.identity_opt) # Add table response = self.tester.post( self.url + str(utils.SERVER_GROUP) + '/' + str(self.server_id) + '/' + str(self.db_id) + '/' + str(self.schema_id) + '/' + str(self.table_id) + '/', - data=json.dumps(data), + data=json.dumps(self.data), content_type='html/json') self.assertEquals(response.status_code, 200) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/test_column_put.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/test_column_put.py index 43071ab85..8bb89b649 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/test_column_put.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/test_column_put.py @@ -26,32 +26,54 @@ from pgadmin.utils import server_utils as server_utils class ColumnPutTestCase(BaseTestGenerator): """This class will update the column under table node.""" scenarios = [ - ('Edit column Node URL', dict(url='/browser/column/obj/', - col_data_type='char')), - ('Edit column with Identity', dict(url='/browser/column/obj/', - col_data_type='bigint', - server_min_version=100000, - identity_opt={ - 'attidentity': 'a', - 'seqincrement': 1, - 'seqstart': 1, - 'seqmin': 1, - 'seqmax': 10, - 'seqcache': 1, - 'seqcycle': True - })), - ('Edit column with Identity', dict(url='/browser/column/obj/', - server_min_version=100000, - col_data_type='bigint', - identity_opt={ - 'attidentity': 'd', - 'seqincrement': 2, - 'seqstart': 2, - 'seqmin': 2, - 'seqmax': 2000, - 'seqcache': 1, - 'seqcycle': True - })) + ('Edit column comments and null constraints', dict( + url='/browser/column/obj/', + col_data_type='char', + data={ + 'attnotnull': True, + 'description': "This is test comment for column" + })), + ('Edit column to Identity column as Always', dict( + url='/browser/column/obj/', + col_data_type='bigint', + server_min_version=100000, + skip_msg='Identity column are not supported by EPAS/PG 10.0 ' + 'and below.', + data={ + 'attnotnull': True, + 'attidentity': 'a', + 'seqincrement': 1, + 'seqstart': 1, + 'seqmin': 1, + 'seqmax': 10, + 'seqcache': 1, + 'seqcycle': True + })), + ('Edit column to Identity column as Default', dict( + url='/browser/column/obj/', + col_data_type='bigint', + server_min_version=100000, + skip_msg='Identity column are not supported by EPAS/PG 10.0 ' + 'and below.', + data={ + 'attnotnull': True, + 'attidentity': 'd', + 'seqincrement': 2, + 'seqstart': 2, + 'seqmin': 2, + 'seqmax': 2000, + 'seqcache': 1, + 'seqcycle': True + })), + ('Edit column Drop Identity by changing constraint type to NONE', + dict(url='/browser/column/obj/', + col_data_type='bigint', + server_min_version=100000, + create_identity_column=True, + skip_msg='Identity column are not supported by EPAS/PG 10.0 ' + 'and below.', + data={'colconstype': 'n'}) + ) ] def setUp(self): @@ -66,9 +88,7 @@ class ColumnPutTestCase(BaseTestGenerator): raise Exception("Could not connect to server to add " "a table.") if server_con["data"]["version"] < self.server_min_version: - message = "Identity columns are not supported by " \ - "PPAS/PG 10.0 and below." - self.skipTest(message) + self.skipTest(self.skip_msg) db_con = database_utils.connect_database(self, utils.SERVER_GROUP, self.server_id, self.db_id) @@ -86,12 +106,16 @@ class ColumnPutTestCase(BaseTestGenerator): self.schema_name, self.table_name) self.column_name = "test_column_put_%s" % (str(uuid.uuid4())[1:8]) - self.column_id = columns_utils.create_column(self.server, - self.db_name, - self.schema_name, - self.table_name, - self.column_name, - self.col_data_type) + + if hasattr(self, 'create_identity_column') and \ + self.create_identity_column: + self.column_id = columns_utils.create_identity_column( + self.server, self.db_name, self.schema_name, + self.table_name, self.column_name, self.col_data_type) + else: + self.column_id = columns_utils.create_column( + self.server, self.db_name, self.schema_name, + self.table_name, self.column_name, self.col_data_type) def runTest(self): """This function will update the column under table node.""" @@ -99,14 +123,11 @@ class ColumnPutTestCase(BaseTestGenerator): self.column_name) if not col_response: raise Exception("Could not find the column to update.") - data = { - "attnum": self.column_id, - "name": self.column_name, - "attnotnull": True, - "description": "This is test comment for column" - } - if hasattr(self, 'identity_opt'): - data.update(self.identity_opt) + self.data.update({ + 'attnum': self.column_id, + 'name': self.column_name, + }) + response = self.tester.put( self.url + str(utils.SERVER_GROUP) + '/' + str(self.server_id) + '/' + @@ -114,7 +135,7 @@ class ColumnPutTestCase(BaseTestGenerator): str(self.schema_id) + '/' + str(self.table_id) + '/' + str(self.column_id), - data=json.dumps(data), + data=json.dumps(self.data), follow_redirects=True) self.assertEquals(response.status_code, 200) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/utils.py index e013699f4..27f9a4300 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/utils.py @@ -63,6 +63,55 @@ def create_column(server, db_name, schema_name, table_name, col_name, raise +def create_identity_column(server, db_name, schema_name, table_name, + col_name, col_data_type='bigint'): + """ + This function creates a column under provided table. + :param server: server details + :type server: dict + :param db_name: database name + :type db_name: str + :param schema_name: schema name + :type schema_name: str + :param table_name: table name + :type table_name: str + :param col_name: column name + :type col_name: str + :param col_data_type: column data type + :type col_data_type: str + :return table_id: table id + :rtype: int + """ + try: + connection = utils.get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + query = "ALTER TABLE %s.%s ADD COLUMN %s %s " \ + "GENERATED ALWAYS AS IDENTITY" % \ + (schema_name, table_name, col_name, col_data_type) + pg_cursor.execute(query) + connection.set_isolation_level(old_isolation_level) + connection.commit() + # Get column position of newly added column + pg_cursor.execute("select attnum from pg_attribute where" + " attname='%s'" % col_name) + col = pg_cursor.fetchone() + col_pos = '' + if col: + col_pos = col[0] + connection.close() + return col_pos + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + def verify_column(server, db_name, col_name): """ This function verifies table exist in database or not. diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/create.sql index e87d1e2e3..0fba3369f 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/create.sql @@ -7,10 +7,10 @@ ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} ADD COLUMN {{conn|qtIdent(data.name)}} {% if is_sql %}{{data.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, data.cltype, data.attlen, data.attprecision, data.hasSqrBracket) }}{% endif %}{% if data.collspcname %} COLLATE {{data.collspcname}}{% endif %}{% if data.attnotnull %} - NOT NULL{% endif %}{% if data.defval is defined and data.defval is not none %} - DEFAULT {{data.defval}}{% endif %}{% if data.attidentity and data.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif data.attidentity and data.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} + NOT NULL{% endif %}{% if data.defval is defined and data.defval is not none and data.defval != '' %} + DEFAULT {{data.defval}}{% endif %}{% if data.colconstype == 'i' %}{% if data.attidentity and data.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif data.attidentity and data.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if data.seqincrement or data.seqcycle or data.seqincrement or data.seqstart or data.seqmin or data.seqmax or data.seqcache %} ( {% endif %} -{% if data.seqincrement is defined and data.seqcycle %} +{% if data.seqcycle is defined and data.seqcycle %} CYCLE {% endif %}{% if data.seqincrement is defined and data.seqincrement|int(-1) > -1 %} INCREMENT {{data.seqincrement|int}} {% endif %}{% if data.seqstart is defined and data.seqstart|int(-1) > -1%} START {{data.seqstart|int}} {% endif %}{% if data.seqmin is defined and data.seqmin|int(-1) > -1%} @@ -18,7 +18,7 @@ MINVALUE {{data.seqmin|int}} {% endif %}{% if data.seqmax is defined and data.se MAXVALUE {{data.seqmax|int}} {% endif %}{% if data.seqcache is defined and data.seqcache|int(-1) > -1%} CACHE {{data.seqcache|int}} {% endif %} {% if data.seqincrement or data.seqcycle or data.seqincrement or data.seqstart or data.seqmin or data.seqmax or data.seqcache %}){% endif %} -{% endif %}; +{% endif %}{% endif %}; {### Add comments ###} {% if data and data.description %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/properties.sql index acfccb096..14ebf5791 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/properties.sql @@ -19,6 +19,7 @@ SELECT att.attname as name, att.*, def.*, pg_catalog.pg_get_expr(def.adbin, def. EXISTS(SELECT 1 FROM pg_constraint WHERE conrelid=att.attrelid AND contype='f' AND att.attnum=ANY(conkey)) As is_fk, (SELECT array_agg(provider || '=' || label) FROM pg_seclabels sl1 WHERE sl1.objoid=att.attrelid AND sl1.objsubid=att.attnum) AS seclabels, (CASE WHEN (att.attnum < 1) THEN true ElSE false END) AS is_sys_column, + (CASE WHEN (att.attidentity in ('a', 'd')) THEN 'i' ELSE 'n' END) AS colconstype, seq.* FROM pg_attribute att JOIN pg_type ty ON ty.oid=atttypid diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/update.sql index 7cd40fc96..581369113 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/update.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/10_plus/update.sql @@ -33,11 +33,11 @@ ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} {% endif %} {### Alter column - add identity ###} -{% if 'attidentity' in data and o_data.attidentity == '' and data.attidentity != o_data.attidentity %} +{% if data.colconstype == 'i' and 'attidentity' in data and o_data.attidentity == '' and data.attidentity != o_data.attidentity %} ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} ALTER COLUMN {% if data.name %}{{conn|qtTypeIdent(data.name)}}{% else %}{{conn|qtTypeIdent(o_data.name)}}{% endif %} {% if data.attidentity == 'a' %}ADD GENERATED ALWAYS AS IDENTITY{% else%}ADD GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if data.seqincrement or data.seqcycle or data.seqincrement or data.seqstart or data.seqmin or data.seqmax or data.seqcache %} ( {% endif %} -{% if data.seqincrement is defined and data.seqcycle %} +{% if data.seqcycle is defined and data.seqcycle %} CYCLE {% endif %}{% if data.seqincrement is defined and data.seqincrement|int(-1) > -1 %} INCREMENT {{data.seqincrement|int}} {% endif %}{% if data.seqstart is defined and data.seqstart|int(-1) > -1%} START {{data.seqstart|int}} {% endif %}{% if data.seqmin is defined and data.seqmin|int(-1) > -1%} @@ -52,10 +52,8 @@ ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} ALTER COLUMN {% if data.name %}{{conn|qtTypeIdent(data.name)}}{% else %}{{conn|qtTypeIdent(o_data.name)}}{% endif %} SET GENERATED {% if data.attidentity == 'a' %}ALWAYS{% else%}BY DEFAULT{% endif %}; {% endif %} - {### Alter column - change identity - sequence options ###} {% if 'attidentity' not in data and (data.seqincrement or data.seqcycle or data.seqincrement or data.seqstart or data.seqmin or data.seqmax or data.seqcache) %} - ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} ALTER COLUMN {% if data.name %}{{conn|qtTypeIdent(data.name)}} {% else %}{{conn|qtTypeIdent(o_data.name)}} {% endif %} {% if data.seqcycle %} @@ -65,11 +63,10 @@ SET START {{data.seqstart|int}} {% endif %}{% if data.seqmin is defined and data SET MINVALUE {{data.seqmin|int}} {% endif %}{% if data.seqmax is defined and data.seqmax|int(-1) > -1%} SET MAXVALUE {{data.seqmax|int}} {% endif %}{% if data.seqcache is defined and data.seqcache|int(-1) > -1%} SET CACHE {{data.seqcache|int}} {% endif %}; + {% endif %} - - -{### Alter column - drop identity ###} -{% if 'attidentity' in data and data.attidentity == '' and o_data.attidentity != '' and data.attidentity != o_data.attidentity %} +{### Alter column - drop identity when column constraint is changed###} +{% if 'colconstype' in data and data.colconstype == 'n' and 'colconstype' in o_data and o_data.colconstype == 'i' %} ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} ALTER COLUMN {% if data.name %}{{conn|qtTypeIdent(data.name)}}{% else %}{{conn|qtTypeIdent(o_data.name)}}{% endif %} DROP IDENTITY; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/create.sql new file mode 100644 index 000000000..108d3b5e4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/create.sql @@ -0,0 +1,46 @@ +{% import 'columns/macros/security.macros' as SECLABEL %} +{% import 'columns/macros/privilege.macros' as PRIVILEGE %} +{% import 'macros/variable.macros' as VARIABLE %} +{% import 'types/macros/get_full_type_sql_format.macros' as GET_TYPE %} +{### Add column ###} +{% if data.name and data.cltype %} +ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} + ADD COLUMN {{conn|qtIdent(data.name)}} {% if is_sql %}{{data.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, data.cltype, data.attlen, data.attprecision, data.hasSqrBracket) }}{% endif %}{% if data.collspcname %} + COLLATE {{data.collspcname}}{% endif %}{% if data.attnotnull %} + NOT NULL{% endif %}{% if data.defval is defined and data.defval is not none and data.defval != '' and data.colconstype != 'g' %} + DEFAULT {{data.defval}}{% endif %}{% if data.colconstype == 'i' %}{% if data.attidentity and data.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif data.attidentity and data.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} +{% if data.seqincrement or data.seqcycle or data.seqincrement or data.seqstart or data.seqmin or data.seqmax or data.seqcache %} ( {% endif %} +{% if data.seqcycle is defined and data.seqcycle %} +CYCLE {% endif %}{% if data.seqincrement is defined and data.seqincrement|int(-1) > -1 %} +INCREMENT {{data.seqincrement|int}} {% endif %}{% if data.seqstart is defined and data.seqstart|int(-1) > -1%} +START {{data.seqstart|int}} {% endif %}{% if data.seqmin is defined and data.seqmin|int(-1) > -1%} +MINVALUE {{data.seqmin|int}} {% endif %}{% if data.seqmax is defined and data.seqmax|int(-1) > -1%} +MAXVALUE {{data.seqmax|int}} {% endif %}{% if data.seqcache is defined and data.seqcache|int(-1) > -1%} +CACHE {{data.seqcache|int}} {% endif %} +{% if data.seqincrement or data.seqcycle or data.seqincrement or data.seqstart or data.seqmin or data.seqmax or data.seqcache %}){% endif %} +{% endif %}{% endif %}{% if data.colconstype == 'g' and data.genexpr and data.genexpr != '' %} GENERATED ALWAYS AS ({{data.genexpr}}) STORED{% endif %}; + +{### Add comments ###} +{% if data and data.description %} +COMMENT ON COLUMN {{conn|qtIdent(data.schema, data.table, data.name)}} + IS {{data.description|qtLiteral}}; + +{% endif %} +{### Add variables to column ###} +{% if data.attoptions %} +ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} + {{ VARIABLE.SET(conn, 'COLUMN', data.name, data.attoptions) }} + +{% endif %} +{### ACL ###} +{% if data.attacl %} +{% for priv in data.attacl %} +{{ PRIVILEGE.APPLY(conn, data.schema, data.table, data.name, priv.grantee, priv.without_grant, priv.with_grant) }} +{% endfor %} +{% endif %} +{### Security Lables ###} +{% if data.seclabels %} +{% for r in data.seclabels %} +{{ SECLABEL.APPLY(conn, 'COLUMN',data.schema, data.table, data.name, r.provider, r.label) }} +{% endfor %} +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/properties.sql new file mode 100644 index 000000000..84b6f3748 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/properties.sql @@ -0,0 +1,48 @@ +SELECT att.attname as name, att.*, def.*, pg_catalog.pg_get_expr(def.adbin, def.adrelid) AS defval, + CASE WHEN att.attndims > 0 THEN 1 ELSE 0 END AS isarray, + format_type(ty.oid,NULL) AS typname, + format_type(ty.oid,att.atttypmod) AS displaytypname, + CASE WHEN ty.typelem > 0 THEN ty.typelem ELSE ty.oid END as elemoid, + tn.nspname as typnspname, et.typname as elemtypname, + ty.typstorage AS defaultstorage, cl.relname, na.nspname, + concat(quote_ident(na.nspname) ,'.', quote_ident(cl.relname)) AS parent_tbl, + att.attstattarget, description, cs.relname AS sername, + ns.nspname AS serschema, + (SELECT count(1) FROM pg_type t2 WHERE t2.typname=ty.typname) > 1 AS isdup, + indkey, coll.collname, nspc.nspname as collnspname , attoptions, + -- Start pgAdmin4, added to save time on client side parsing + CASE WHEN length(coll.collname) > 0 AND length(nspc.nspname) > 0 THEN + concat(quote_ident(nspc.nspname),'.',quote_ident(coll.collname)) + ELSE '' END AS collspcname, + format_type(ty.oid,att.atttypmod) AS cltype, + -- End pgAdmin4 + EXISTS(SELECT 1 FROM pg_constraint WHERE conrelid=att.attrelid AND contype='f' AND att.attnum=ANY(conkey)) As is_fk, + (SELECT array_agg(provider || '=' || label) FROM pg_seclabels sl1 WHERE sl1.objoid=att.attrelid AND sl1.objsubid=att.attnum) AS seclabels, + (CASE WHEN (att.attnum < 1) THEN true ElSE false END) AS is_sys_column, + (CASE WHEN (att.attidentity in ('a', 'd')) THEN 'i' WHEN (att.attgenerated in ('s')) THEN 'g' ELSE 'n' END) AS colconstype, + (CASE WHEN (att.attgenerated in ('s')) THEN pg_catalog.pg_get_expr(def.adbin, def.adrelid) END) AS genexpr, + seq.* +FROM pg_attribute att + JOIN pg_type ty ON ty.oid=atttypid + JOIN pg_namespace tn ON tn.oid=ty.typnamespace + JOIN pg_class cl ON cl.oid=att.attrelid + JOIN pg_namespace na ON na.oid=cl.relnamespace + LEFT OUTER JOIN pg_type et ON et.oid=ty.typelem + LEFT OUTER JOIN pg_attrdef def ON adrelid=att.attrelid AND adnum=att.attnum + LEFT OUTER JOIN pg_description des ON (des.objoid=att.attrelid AND des.objsubid=att.attnum AND des.classoid='pg_class'::regclass) + LEFT OUTER JOIN (pg_depend JOIN pg_class cs ON classid='pg_class'::regclass AND objid=cs.oid AND cs.relkind='S') ON refobjid=att.attrelid AND refobjsubid=att.attnum + LEFT OUTER JOIN pg_namespace ns ON ns.oid=cs.relnamespace + LEFT OUTER JOIN pg_index pi ON pi.indrelid=att.attrelid AND indisprimary + LEFT OUTER JOIN pg_collation coll ON att.attcollation=coll.oid + LEFT OUTER JOIN pg_namespace nspc ON coll.collnamespace=nspc.oid + LEFT OUTER JOIN pg_sequence seq ON cs.oid=seq.seqrelid +WHERE att.attrelid = {{tid}}::oid +{% if clid %} + AND att.attnum = {{clid}}::int +{% endif %} +{### To show system objects ###} +{% if not show_sys_objects %} + AND att.attnum > 0 +{% endif %} + AND att.attisdropped IS FALSE + ORDER BY att.attnum; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/default/create.sql index 50a6a5bd4..1e2f5cfa9 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/default/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/default/create.sql @@ -7,7 +7,7 @@ ALTER TABLE {{conn|qtIdent(data.schema, data.table)}} ADD COLUMN {{conn|qtIdent(data.name)}} {% if is_sql %}{{data.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, data.cltype, data.attlen, data.attprecision, data.hasSqrBracket) }}{% endif %}{% if data.collspcname %} COLLATE {{data.collspcname}}{% endif %}{% if data.attnotnull %} - NOT NULL{% endif %}{% if data.defval is defined and data.defval is not none %} + NOT NULL{% endif %}{% if data.defval is defined and data.defval is not none and data.defval != '' %} DEFAULT {{data.defval}}{% endif %}; {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/10_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/10_plus/create.sql index 1f4c83877..8ba1d882b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/10_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/10_plus/create.sql @@ -42,11 +42,11 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE {{conn|qtIdent(data {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none %} DEFAULT {{c.defval}}{% endif %} -{% if c.attidentity and c.attidentity != '' %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' %} DEFAULT {{c.defval}}{% endif %} +{% if c.colconstype == 'i' and c.attidentity and c.attidentity != '' %} {% if c.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif c.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if c.seqincrement or c.seqcycle or c.seqincrement or c.seqstart or c.seqmin or c.seqmax or c.seqcache %} ( {% endif %} -{% if c.seqincrement is defined and c.seqcycle %} +{% if c.seqcycle is defined and c.seqcycle %} CYCLE {% endif %}{% if c.seqincrement is defined and c.seqincrement|int(-1) > -1 %} INCREMENT {{c.seqincrement|int}} {% endif %}{% if c.seqstart is defined and c.seqstart|int(-1) > -1%} START {{c.seqstart|int}} {% endif %}{% if c.seqmin is defined and c.seqmin|int(-1) > -1%} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/12_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/12_plus/create.sql index 7acbd0083..c4ba99216 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/12_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/12_plus/create.sql @@ -46,11 +46,11 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE {{conn|qtIdent(data {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none %} DEFAULT {{c.defval}}{% endif %} -{% if c.attidentity and c.attidentity != '' %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' and c.colconstype != 'g' %} DEFAULT {{c.defval}}{% endif %} +{% if c.colconstype == 'i' and c.attidentity and c.attidentity != '' %} {% if c.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif c.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if c.seqincrement or c.seqcycle or c.seqincrement or c.seqstart or c.seqmin or c.seqmax or c.seqcache %} ( {% endif %} -{% if c.seqincrement is defined and c.seqcycle %} +{% if c.seqcycle is defined and c.seqcycle %} CYCLE {% endif %}{% if c.seqincrement is defined and c.seqincrement|int(-1) > -1 %} INCREMENT {{c.seqincrement|int}} {% endif %}{% if c.seqstart is defined and c.seqstart|int(-1) > -1%} START {{c.seqstart|int}} {% endif %}{% if c.seqmin is defined and c.seqmin|int(-1) > -1%} @@ -59,6 +59,7 @@ MAXVALUE {{c.seqmax|int}} {% endif %}{% if c.seqcache is defined and c.seqcache| CACHE {{c.seqcache|int}} {% endif %} {% if c.seqincrement or c.seqcycle or c.seqincrement or c.seqstart or c.seqmin or c.seqmax or c.seqcache %}){% endif %} {% endif %} +{% if c.colconstype == 'g' and c.genexpr and c.genexpr != '' %} GENERATED ALWAYS AS ({{c.genexpr}}) STORED{% endif %} {% if not loop.last %}, {% endif %} {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/create.sql index 1aa793475..5a5519714 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/create.sql @@ -42,7 +42,7 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE {{conn|qtIdent(data {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none %} DEFAULT {{c.defval}}{% endif %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' %} DEFAULT {{c.defval}}{% endif %} {% if not loop.last %}, {% endif %} {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/gpdb_5.0_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/gpdb_5.0_plus/create.sql index 57a0d11a9..b6ff1f188 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/gpdb_5.0_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/gpdb_5.0_plus/create.sql @@ -42,7 +42,7 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE {{conn|qtIdent(data {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none %} DEFAULT {{c.defval}}{% endif %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' %} DEFAULT {{c.defval}}{% endif %} {% if not loop.last %}, {% endif %} {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_table_add.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_table_add.py index 6b97b253f..599eaec77 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_table_add.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_table_add.py @@ -18,6 +18,7 @@ from pgadmin.utils import server_utils as server_utils from pgadmin.utils.route import BaseTestGenerator from regression import parent_node_dict from regression.python_test_utils import test_utils as utils +from . import utils as tables_utils class TableAddTestCase(BaseTestGenerator): @@ -28,7 +29,9 @@ class TableAddTestCase(BaseTestGenerator): ('Create Range partitioned table with 2 partitions', dict(url='/browser/table/obj/', server_min_version=100000, - partition_type='range' + partition_type='range', + skip_msg='Partitioned table are not supported by ' + 'PPAS/PG 10.0 and below.' ) ), ('Create Range partitioned table with 1 default and 2' @@ -36,21 +39,104 @@ class TableAddTestCase(BaseTestGenerator): dict(url='/browser/table/obj/', server_min_version=110000, partition_type='range', - is_default=True + is_default=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, - partition_type='list' + partition_type='list', + 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, - partition_type='hash' + partition_type='hash', + skip_msg='Hash Partition are not supported by ' + 'PPAS/PG 11.0 and below.' ) + ), + ('Create Table with Identity columns', + dict(url='/browser/table/obj/', + server_min_version=100000, + skip_msg='Identity columns are not supported by ' + 'PPAS/PG 10.0 and below.', + columns=[{ + 'name': 'iden_always', + 'cltype': 'bigint', + 'attacl': [], + 'is_primary_key': False, + 'attnotnull': True, + 'attlen': None, + 'attprecision': None, + 'attoptions': [], + 'seclabels': [], + 'colconstype': 'i', + 'attidentity': 'a', + 'seqincrement': 1, + 'seqstart': 1, + 'seqmin': 1, + 'seqmax': 10, + 'seqcache': 1, + 'seqcycle': True + }, { + 'name': 'iden_default', + 'cltype': 'bigint', + 'attacl': [], + 'is_primary_key': False, + 'attnotnull': True, + 'attlen': None, + 'attprecision': None, + 'attoptions': [], + 'seclabels': [], + 'colconstype': 'i', + 'attidentity': 'd', + 'seqincrement': 2, + 'seqstart': 2, + 'seqmin': 2, + 'seqmax': 2000, + 'seqcache': 1, + 'seqcycle': True + }]) + ), + ('Create Table with Generated columns', + dict(url='/browser/table/obj/', + server_min_version=120000, + skip_msg='Generated columns are not supported by ' + 'PPAS/PG 12.0 and below.', + columns=[{ + 'name': 'm1', + 'cltype': 'bigint', + 'attacl': [], + 'is_primary_key': False, + 'attoptions': [], + 'seclabels': [] + }, { + 'name': 'm2', + 'cltype': 'bigint', + 'attacl': [], + 'is_primary_key': False, + 'attoptions': [], + 'seclabels': [] + }, { + 'name': 'genrated', + 'cltype': 'bigint', + 'attacl': [], + 'is_primary_key': False, + 'attnotnull': True, + 'attlen': None, + 'attprecision': None, + 'attoptions': [], + 'seclabels': [], + 'colconstype': 'g', + 'genexpr': 'm1*m2' + }]) ) + ] def setUp(self): @@ -70,145 +156,48 @@ class TableAddTestCase(BaseTestGenerator): if not schema_response: raise Exception("Could not find the schema to add a table.") - self.is_partition = False if hasattr(self, 'server_min_version'): server_con = server_utils.connect_server(self, self.server_id) if not server_con["info"] == "Server connected.": raise Exception("Could not connect to server to add " "partitioned table.") if server_con["data"]["version"] < self.server_min_version: - message = "Partitioned table are not supported by " \ - "PPAS/PG 10.0 and below." - self.skipTest(message) - else: - self.is_partition = True + self.skipTest(self.skip_msg) def runTest(self): """ This function will add table under schema node. """ db_user = self.server["username"] self.table_name = "test_table_add_%s" % (str(uuid.uuid4())[1:8]) - data = { - "check_constraint": [], - "coll_inherits": "[]", - "columns": [ - { - "name": "empno", - "cltype": "numeric", - "attacl": [], - "is_primary_key": False, - "attoptions": [], - "seclabels": [] - }, - { - "name": "empname", - "cltype": "character[]", - "attacl": [], - "is_primary_key": False, - "attoptions": [], - "seclabels": [] - }, - { - "name": "DOJ", - "cltype": "date", - "attacl": [], - "is_primary_key": False, - "attoptions": [], - "seclabels": [] - } - ], - "exclude_constraint": [], - "fillfactor": "", - "hastoasttable": True, - "like_constraints": True, - "like_default_value": True, - "like_relation": "pg_catalog.pg_namespace", + # Get the common data + data = tables_utils.get_table_common_data() + data.update({ "name": self.table_name, - "primary_key": [], - "relacl": [ - { - "grantee": db_user, - "grantor": db_user, - "privileges": - [ - { - "privilege_type": "a", - "privilege": True, - "with_grant": True - }, - { - "privilege_type": "r", - "privilege": True, - "with_grant": False - }, - { - "privilege_type": "w", - "privilege": True, - "with_grant": False - } - ] - } - ], - "relhasoids": True, "relowner": db_user, "schema": self.schema_name, - "seclabels": [], - "spcname": "pg_default", - "unique_constraint": [], - "vacuum_table": [ - { - "name": "autovacuum_analyze_scale_factor" - }, - { - "name": "autovacuum_analyze_threshold" - }, - { - "name": "autovacuum_freeze_max_age" - }, - { - "name": "autovacuum_vacuum_cost_delay" - }, - { - "name": "autovacuum_vacuum_cost_limit" - }, - { - "name": "autovacuum_vacuum_scale_factor" - }, - { - "name": "autovacuum_vacuum_threshold" - }, - { - "name": "autovacuum_freeze_min_age" - }, - { - "name": "autovacuum_freeze_table_age" - } - ], - "vacuum_toast": [ - { - "name": "autovacuum_freeze_max_age" - }, - { - "name": "autovacuum_vacuum_cost_delay" - }, - { - "name": "autovacuum_vacuum_cost_limit" - }, - { - "name": "autovacuum_vacuum_scale_factor" - }, - { - "name": "autovacuum_vacuum_threshold" - }, - { - "name": "autovacuum_freeze_min_age" - }, - { - "name": "autovacuum_freeze_table_age" - } - ] - } + "relacl": [{ + "grantee": db_user, + "grantor": db_user, + "privileges": [{ + "privilege_type": "a", + "privilege": True, + "with_grant": True + }, { + "privilege_type": "r", + "privilege": True, + "with_grant": False + }, { + "privilege_type": "w", + "privilege": True, + "with_grant": False + }] + }] + }) - if self.is_partition: + # If column is provided in the scenario then use those columns + if hasattr(self, 'columns'): + data['columns'] = self.columns + + if hasattr(self, 'partition_type'): data['partition_type'] = self.partition_type data['is_partitioned'] = True if self.partition_type == 'range': diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/utils.py index 51dd5d98c..148e15cfe 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/utils.py @@ -242,3 +242,81 @@ def set_partition_data(server, db_name, schema_name, table_name, }] } ) + + +def get_table_common_data(): + """ + This function will return the common data used to create a table + :return: + """ + return { + "check_constraint": [], + "coll_inherits": "[]", + "columns": [{ + "name": "empno", + "cltype": "numeric", + "attacl": [], + "is_primary_key": False, + "attoptions": [], + "seclabels": [] + }, { + "name": "empname", + "cltype": "character[]", + "attacl": [], + "is_primary_key": False, + "attoptions": [], + "seclabels": [] + }, { + "name": "DOJ", + "cltype": "date", + "attacl": [], + "is_primary_key": False, + "attoptions": [], + "seclabels": [] + }], + "exclude_constraint": [], + "fillfactor": "", + "hastoasttable": True, + "like_constraints": True, + "like_default_value": True, + "like_relation": "pg_catalog.pg_namespace", + "primary_key": [], + "relhasoids": True, + "seclabels": [], + "spcname": "pg_default", + "unique_constraint": [], + "vacuum_table": [{ + "name": "autovacuum_analyze_scale_factor" + }, { + "name": "autovacuum_analyze_threshold" + }, { + "name": "autovacuum_freeze_max_age" + }, { + "name": "autovacuum_vacuum_cost_delay" + }, { + "name": "autovacuum_vacuum_cost_limit" + }, { + "name": "autovacuum_vacuum_scale_factor" + }, { + "name": "autovacuum_vacuum_threshold" + }, { + "name": "autovacuum_freeze_min_age" + }, { + "name": "autovacuum_freeze_table_age" + }], + "vacuum_toast": [{ + "name": "autovacuum_freeze_max_age" + }, { + "name": "autovacuum_vacuum_cost_delay" + }, { + "name": "autovacuum_vacuum_cost_limit" + }, { + "name": "autovacuum_vacuum_scale_factor" + }, { + "name": "autovacuum_vacuum_threshold" + }, { + "name": "autovacuum_freeze_min_age" + }, { + "name": "autovacuum_freeze_table_age" + }] + } diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js index 012a7432b..8e69cfb81 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -448,11 +448,11 @@ define([ template: _.template([ '', '
', - '
', + '
', ' <% for (var i=0; i < options.length; i++) { %>', ' <% var option = options[i]; %>', - '
', @@ -466,7 +466,37 @@ define([ return this.formatter.toRaw(this.$el.find('input[type="radio"]:checked').attr('value'), this.model); }, render: function() { - Backform.RadioControl.prototype.render.apply(this, arguments); + var field = _.defaults(this.field.toJSON(), this.defaults), + attributes = this.model.toJSON(), + attrArr = field.name.split('.'), + name = attrArr.shift(), + path = attrArr.join('.'), + rawValue = this.keyPathAccessor(attributes[name], path), + data = _.extend(field, { + rawValue: rawValue, + value: this.formatter.fromRaw(rawValue, this.model), + attributes: attributes, + formatter: this.formatter, + }), + // Evaluate the disabled, visible, and required option + evalF = function evalF(f, d, m) { + return _.isFunction(f) ? !!f.apply(d, [m]) : !!f; + }; + + _.extend(data, { + disabled: evalF(data.disabled, data, this.model), + visible: evalF(data.visible, data, this.model), + required: evalF(data.required, data, this.model), + }); // Clean up first + + data.options = _.isFunction(data.options) ? + data.options.apply(data, [this.model]) : data.options; + + this.$el.removeClass(Backform.hiddenClassName); + if (!data.visible) this.$el.addClass(Backform.hiddenClassName); + this.$el.html(this.template(data)).addClass(field.name); + this.updateInvalid(); + this.$el.find('.btn').on('keyup', (e)=>{ switch(e.keyCode) { case 32: /* Spacebar click */ diff --git a/web/pgadmin/static/scss/_bootstrap.overrides.scss b/web/pgadmin/static/scss/_bootstrap.overrides.scss index 55fc5afab..3b2df9fd6 100644 --- a/web/pgadmin/static/scss/_bootstrap.overrides.scss +++ b/web/pgadmin/static/scss/_bootstrap.overrides.scss @@ -315,3 +315,11 @@ td.switch-cell > div.toggle { background-color: $color-primary-light; color: $color-primary; } + +.btn-group.pgadmin-controls-radio-none.disabled { + pointer-events: none; +} + +.btn-group.pgadmin-controls-radio-none > label.btn.btn-primary.disabled { + pointer-events: none; +}