Add support for INCLUDE columns on indexes and index constraints with PG 11+. Fixes #3462

This commit is contained in:
Aditya Toshniwal 2018-07-19 13:13:37 +01:00 committed by Dave Page
parent a86604160a
commit c353135a3a
57 changed files with 1164 additions and 29 deletions

View File

@ -48,6 +48,8 @@ Use the fields in the *Columns* tab to to specify the column(s) to which the con
* Use the *NULLs order* column to specify the placement of NULL values (when sorted). Specify *FIRST* or *LAST*.
* Use the drop-down list next to *Operator* to specify a comparison or conditional operator.
Use *Include columns* field to specify columns for *INCLUDE* clause of the constraint. This option is available in Postgres 11 and later.
Click the *SQL* tab to continue.
Your entries in the *Exclusion Constraint* dialog generate a SQL command (see an example below). Use the *SQL* tab for review; revisit or switch tabs to make any changes to the SQL command.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -55,6 +55,8 @@ Use the context-sensitive fields in the *Columns* panel to specify which column(
* Use the drop-down listbox in the *Collation* field to select a collation to use for the index.
Use *Include columns* field to specify columns for *INCLUDE* clause of the index. This option is available in Postgres 11 and later.
Click the *SQL* tab to continue.
Your entries in the *Index* dialog generate a SQL command (see an example below). Use the *SQL* tab for review; revisit or switch tabs to make any changes to the SQL command.

View File

@ -23,6 +23,7 @@ Click the *Definition* tab to continue.
Use the fields in the *Definition* tab to define the primary key constraint:
* Click inside the *Columns* field and select one or more column names from the drop-down listbox. To delete a selection, click the *x* to the left of the column name. The primary key constraint should be different from any unique constraint defined for the same table; the selected column(s) for the constraints must be distinct.
* Use *Include columns* field to specify columns for *INCLUDE* clause of the index. This option is available in Postgres 11 and later.
* Select the name of the tablespace in which the primary key constraint will reside from the drop-down listbox in the *Tablespace* field.
* Select the name of an index from the drop-down listbox in the *Index* field. This field is optional. Adding a primary key will automatically create a unique B-tree index on the column or group of columns listed in the primary key, and will force the column(s) to be marked NOT NULL.
* Use the *Fill Factor* field to specify a fill factor for the table and index. The fill factor for a table is a percentage between 10 and 100. 100 (complete packing) is the default.

View File

@ -23,6 +23,7 @@ Click the *Definition* tab to continue.
Use the fields in the *Definition* tab to define the unique constraint:
* Click inside the *Columns* field and select one or more column names from the drop-down listbox. To delete a selection, click the *x* to the left of the column name. The unique constraint should be different from the primary key constraint defined for the same table; the selected column(s) for the constraints must be distinct.
* Use *Include columns* field to specify columns for *INCLUDE* clause of the constraint. This option is available in Postgres 11 and later.
* Select the name of the tablespace in which the unique constraint will reside from the drop-down listbox in the *Tablespace* field.
* Select the name of an index from the drop-down listbox in the *Index* field. This field is optional. Adding a unique constraint will automatically create a unique B-tree index on the column or group of columns listed in the constraint, and will force the column(s) to be marked NOT NULL.
* Use the *Fill Factor* field to specify a fill factor for the table and index. The fill factor for a table is a percentage between 10 and 100. 100 (complete packing) is the default.

View File

@ -301,7 +301,7 @@ class ExclusionConstraintView(PGChildNodeView):
sql = render_template(
"/".join([self.template_path, 'get_constraint_cols.sql']),
cid=exid,
colcnt=result['indnatts'])
colcnt=result['col_count'])
status, res = self.conn.execute_dict(sql)
if not status:
@ -326,6 +326,18 @@ class ExclusionConstraintView(PGChildNodeView):
result['columns'] = columns
# Add Include details of the index supported for PG-11+
if self.manager.version >= 110000:
sql = render_template(
"/".join([self.template_path, 'get_constraint_include.sql']),
cid=exid)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
result['include'] = [col['colname'] for col in res['rows']]
return ajax_response(
response=result,
status=200
@ -875,7 +887,7 @@ class ExclusionConstraintView(PGChildNodeView):
sql = render_template(
"/".join([self.template_path, 'get_constraint_cols.sql']),
cid=exid,
colcnt=data['indnatts'])
colcnt=data['col_count'])
status, res = self.conn.execute_dict(sql)
if not status:
@ -899,6 +911,20 @@ class ExclusionConstraintView(PGChildNodeView):
data['columns'] = columns
# Add Include details of the index supported for PG-11+
if self.manager.version >= 110000:
sql = render_template(
"/".join(
[self.template_path, 'get_constraint_include.sql']
),
cid=exid)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
data['include'] = [col['colname'] for col in res['rows']]
if not data['amname'] or data['amname'] == '':
data['amname'] = 'btree'

View File

@ -650,6 +650,7 @@ define('pgadmin.node.exclusion_constraint', [
condeferrable: undefined,
condeferred: undefined,
columns: [],
include: [],
},
// Define the schema for the exclusion constraint node
@ -891,6 +892,103 @@ define('pgadmin.node.exclusion_constraint', [
Backgrid.StringCell.prototype.remove.apply(this, arguments);
},
}),
},{
id: 'include', label: gettext('Include columns'),
type: 'array', group: gettext('Columns'),
editable: false,
canDelete: true, canAdd: true, mode: ['properties', 'create', 'edit'],
visible: function(m) {
/* In table properties, m.node_info is not available */
m = m.top;
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
&& !_.isUndefined(m.node_info.server.version) &&
m.node_info.server.version >= 110000)
return true;
return false;
},
control: Backform.MultiSelectAjaxControl.extend({
defaults: _.extend(
{},
Backform.NodeListByNameControl.prototype.defaults,
{
select2: {
allowClear: false,
width: 'style',
multiple: true,
placeholder: gettext('Select the column(s)'),
},
}
),
initialize: function() {
// Here we will decide if we need to call URL
// Or fetch the data from parent columns collection
var self = this;
if(this.model.handler) {
Backform.Select2Control.prototype.initialize.apply(this, arguments);
// Do not listen for any event(s) for existing constraint.
if (_.isUndefined(self.model.get('oid'))) {
var tableCols = self.model.top.get('columns');
self.listenTo(tableCols, 'remove' , self.resetColOptions);
self.listenTo(tableCols, 'change:name', self.resetColOptions);
}
self.custom_options();
} else {
Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments);
}
},
resetColOptions: function() {
var self = this;
setTimeout(function () {
self.custom_options();
self.render.apply(self);
}, 50);
},
custom_options: function() {
// We will add all the columns entered by user in table model
var columns = this.model.top.get('columns'),
added_columns_from_tables = [];
if (columns.length > 0) {
_.each(columns.models, function(m) {
var col = m.get('name');
if(!_.isUndefined(col) && !_.isNull(col)) {
added_columns_from_tables.push(
{label: col, value: col, image:'icon-column'}
);
}
});
}
// Set the values in to options so that user can select
this.field.set('options', added_columns_from_tables);
},
}),
deps: ['index'], node: 'column',
disabled: function(m) {
// If we are in table edit mode then
if (_.has(m, 'top') && !_.isUndefined(m.top)
&& !m.top.isNew()) {
// If OID is undefined then user is trying to add
// new constraint which should be allowed for Unique
return !_.isUndefined(m.get('oid'));
}
// We can't update columns of existing index constraint.
if (!m.isNew()) {
return true;
}
// Disable if index is selected.
var index = m.get('index');
if(_.isUndefined(index) || index == '') {
return false;
} else {
var col = m.get('columns');
col.reset();
return true;
}
},
}],
validate: function() {
this.errorModel.clear();

View File

@ -0,0 +1,8 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2018, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

View File

@ -0,0 +1,80 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2018, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
import utils as tables_utils
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
utils as schema_utils
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_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 exclusion_utils
class ExclusionConstraintAddTestCase(BaseTestGenerator):
"""This class will add new exclusion constraint to existing table"""
scenarios = [
('Add Exclusion Constraint URL',
dict(url='/browser/exclusion_constraint/obj/'))
]
def setUp(self):
self.db_name = parent_node_dict["database"][-1]["db_name"]
schema_info = parent_node_dict["schema"][-1]
self.server_id = schema_info["server_id"]
self.db_id = schema_info["db_id"]
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.server_id, self.db_id)
if not db_con['data']["connected"]:
raise Exception("Could not connect to database to add a table.")
self.schema_id = schema_info["schema_id"]
self.schema_name = schema_info["schema_name"]
schema_response = schema_utils.verify_schemas(self.server,
self.db_name,
self.schema_name)
if not schema_response:
raise Exception("Could not find the schema to add a table.")
self.table_name = "table_for_exclusion_%s" % (str(uuid.uuid4())[1:8])
self.table_id = tables_utils.create_table(self.server, self.db_name,
self.schema_name,
self.table_name)
def runTest(self):
"""This function will add exclusion constraint to existing table."""
self.index_name = "test_index_add_%s" % (str(uuid.uuid4())[1:8])
data = {"name": self.index_name,
"spcname": "pg_default",
"amname": "btree",
"columns": [
{"column": "id", "sort_order": False, "nulls": False,
"operator": "="}],
"include": ["name"]
}
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),
content_type='html/json')
self.assertEquals(response.status_code, 200)
index_response = exclusion_utils.verify_exclusion_constraint(
self.server, self.db_name, self.index_name)
if not index_response:
raise Exception("Could not find the constraint added.")
def tearDown(self):
# Disconnect the database
database_utils.disconnect_database(self, self.server_id, self.db_id)

View File

@ -0,0 +1,79 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2018, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import uuid
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
import utils as tables_utils
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
utils as schema_utils
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_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 exclusion_utils
class ExclusionConstraintDeleteTestCase(BaseTestGenerator):
"""This class will delete the existing exclusion constraint of table."""
scenarios = [
('Delete Exclusion Constraint Node URL',
dict(url='/browser/exclusion_constraint/obj/'))
]
def setUp(self):
self.db_name = parent_node_dict["database"][-1]["db_name"]
schema_info = parent_node_dict["schema"][-1]
self.server_id = schema_info["server_id"]
self.db_id = schema_info["db_id"]
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.server_id, self.db_id)
if not db_con['data']["connected"]:
raise Exception("Could not connect to database to add a table.")
self.schema_id = schema_info["schema_id"]
self.schema_name = schema_info["schema_name"]
schema_response = schema_utils.verify_schemas(self.server,
self.db_name,
self.schema_name)
if not schema_response:
raise Exception("Could not find the schema to add a table.")
self.table_name = "table_exclusion_%s" % (str(uuid.uuid4())[1:8])
self.table_id = tables_utils.create_table(self.server, self.db_name,
self.schema_name,
self.table_name)
self.index_name = "test_exclusion_delete_%s" % (str(uuid.uuid4())[1:8])
self.index_id = exclusion_utils.create_exclusion_constraint(
self.server, self.db_name, self.schema_name, self.table_name,
self.index_name
)
def runTest(self):
"""This function will delete exclusion constraint."""
index_response = exclusion_utils.verify_exclusion_constraint(
self.server, self.db_name, self.index_name)
if not index_response:
raise Exception("Could not find the constraint to delete.")
response = self.tester.delete(self.url + str(utils.SERVER_GROUP) +
'/' + str(self.server_id) + '/' +
str(self.db_id) + '/' +
str(self.schema_id) + '/' +
str(self.table_id) + '/' +
str(self.index_id),
follow_redirects=True)
self.assertEquals(response.status_code, 200)
index_response = exclusion_utils.verify_exclusion_constraint(
self.server, self.db_name, self.index_name)
if index_response:
raise Exception("Constraint is not deleted.")
def tearDown(self):
# Disconnect the database
database_utils.disconnect_database(self, self.server_id, self.db_id)

View File

@ -0,0 +1,69 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2018, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import uuid
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
import utils as tables_utils
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
utils as schema_utils
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_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 exclusion_utils
class ExclusionGetTestCase(BaseTestGenerator):
"""This class will fetch the existing exclusion constraint"""
scenarios = [
('Fetch Exclusion Constraint',
dict(url='/browser/exclusion_constraint/obj/'))
]
def setUp(self):
self.db_name = parent_node_dict["database"][-1]["db_name"]
schema_info = parent_node_dict["schema"][-1]
self.server_id = schema_info["server_id"]
self.db_id = schema_info["db_id"]
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.server_id, self.db_id)
if not db_con['data']["connected"]:
raise Exception("Could not connect to database to add a table.")
self.schema_id = schema_info["schema_id"]
self.schema_name = schema_info["schema_name"]
schema_response = schema_utils.verify_schemas(self.server,
self.db_name,
self.schema_name)
if not schema_response:
raise Exception("Could not find the schema to add a table.")
self.table_name = "table_exclusion_%s" % (str(uuid.uuid4())[1:8])
self.table_id = tables_utils.create_table(self.server, self.db_name,
self.schema_name,
self.table_name)
self.index_name = "test_exclusion_get_%s" % (str(uuid.uuid4())[1:8])
self.index_id = exclusion_utils.create_exclusion_constraint(
self.server, self.db_name, self.schema_name, self.table_name,
self.index_name
)
def runTest(self):
"""This function will fetch the existing exclusion constraint."""
response = self.tester.get(
"{0}{1}/{2}/{3}/{4}/{5}/{6}".format(self.url, utils.SERVER_GROUP,
self.server_id, self.db_id,
self.schema_id, self.table_id,
self.index_id),
follow_redirects=True)
self.assertEquals(response.status_code, 200)
def tearDown(self):
# Disconnect the database
database_utils.disconnect_database(self, self.server_id, self.db_id)

View File

@ -0,0 +1,77 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2018, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
import utils as tables_utils
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
utils as schema_utils
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_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 exclusion_utils
class IndexesUpdateTestCase(BaseTestGenerator):
"""This class will update the existing exclusion constraint."""
scenarios = [
('Put exclusion constraint URL',
dict(url='/browser/exclusion_constraint/obj/'))
]
def setUp(self):
self.db_name = parent_node_dict["database"][-1]["db_name"]
schema_info = parent_node_dict["schema"][-1]
self.server_id = schema_info["server_id"]
self.db_id = schema_info["db_id"]
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.server_id, self.db_id)
if not db_con['data']["connected"]:
raise Exception("Could not connect to database to add a table.")
self.schema_id = schema_info["schema_id"]
self.schema_name = schema_info["schema_name"]
schema_response = schema_utils.verify_schemas(self.server,
self.db_name,
self.schema_name)
if not schema_response:
raise Exception("Could not find the schema to add a table.")
self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8])
self.table_id = tables_utils.create_table(self.server, self.db_name,
self.schema_name,
self.table_name)
self.index_name = "test_exclusion_delete_%s" % (str(uuid.uuid4())[1:8])
self.index_id = exclusion_utils.create_exclusion_constraint(
self.server, self.db_name, self.schema_name, self.table_name,
self.index_name
)
def runTest(self):
"""This function will update an existing exclusion constraint"""
index_response = exclusion_utils.verify_exclusion_constraint(
self.server, self.db_name, self.index_name)
if not index_response:
raise Exception("Could not find the exclusion constraint.")
data = {"oid": self.index_id,
"comment": "This is test comment for index"}
response = self.tester.put(
"{0}{1}/{2}/{3}/{4}/{5}/{6}".format(self.url, utils.SERVER_GROUP,
self.server_id, self.db_id,
self.schema_id, self.table_id,
self.index_id),
data=json.dumps(data),
follow_redirects=True)
self.assertEquals(response.status_code, 200)
def tearDown(self):
# Disconnect the database
database_utils.disconnect_database(self, self.server_id, self.db_id)

View File

@ -0,0 +1,89 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2018, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from __future__ import print_function
import sys
import traceback
from regression.python_test_utils import test_utils as utils
def create_exclusion_constraint(server, db_name, schema_name, table_name,
key_name):
"""
This function creates a exclusion constraint 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 key_name: test name for key
:type key_name: str
:return oid: key constraint 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 CONSTRAINT %s EXCLUDE USING btree(" \
"id ASC NULLS FIRST WITH =)" % \
(schema_name, table_name, key_name)
pg_cursor.execute(query)
connection.set_isolation_level(old_isolation_level)
connection.commit()
# Get oid of newly added index constraint
pg_cursor.execute(
"SELECT conindid FROM pg_constraint where conname='%s'" % key_name)
index_constraint = pg_cursor.fetchone()
connection.close()
oid = index_constraint[0]
return oid
except Exception:
traceback.print_exc(file=sys.stderr)
def verify_exclusion_constraint(server, db_name, index_name):
"""
This function verifies index exist or not.
:param server: server details
:type server: dict
:param db_name: database name
:type db_name: str
:param index_name: index name
:type index_name: str
:return table: table record from database
:rtype: tuple
"""
try:
connection = utils.get_db_connection(db_name,
server['username'],
server['db_password'],
server['host'],
server['port'],
server['sslmode'])
pg_cursor = connection.cursor()
pg_cursor.execute("select * from pg_class where relname='%s'" %
index_name)
index_record = pg_cursor.fetchone()
connection.close()
return index_record
except Exception:
traceback.print_exc(file=sys.stderr)
raise

View File

@ -1118,7 +1118,7 @@ class ForeignKeyConstraintView(PGChildNodeView):
sql = render_template(
"/".join([self.template_path, 'get_cols.sql']),
cid=costrnt['oid'],
colcnt=costrnt['indnatts'])
colcnt=costrnt['col_count'])
status, rest = self.conn.execute_dict(sql)
if not status:

View File

@ -263,7 +263,8 @@ class IndexConstraintView(PGChildNodeView):
kwargs['sid']
)
self.conn = self.manager.connection(did=kwargs['did'])
self.template_path = 'index_constraint/sql'
self.template_path = 'index_constraint/sql/#{0}#'\
.format(self.manager.version)
# We need parent's name eg table name and schema name
SQL = render_template("/".join([self.template_path,
@ -323,7 +324,7 @@ class IndexConstraintView(PGChildNodeView):
sql = render_template(
"/".join([self.template_path, 'get_constraint_cols.sql']),
cid=cid,
colcnt=result['indnatts'])
colcnt=result['col_count'])
status, res = self.conn.execute_dict(sql)
if not status:
@ -335,6 +336,18 @@ class IndexConstraintView(PGChildNodeView):
result['columns'] = columns
# Add Include details of the index supported for PG-11+
if self.manager.version >= 110000:
sql = render_template(
"/".join([self.template_path, 'get_constraint_include.sql']),
cid=cid)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
result['include'] = [col['colname'] for col in res['rows']]
return ajax_response(
response=result,
status=200
@ -384,7 +397,8 @@ class IndexConstraintView(PGChildNodeView):
"""
self.manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
self.conn = self.manager.connection(did=did)
self.template_path = 'index_constraint/sql'
self.template_path = 'index_constraint/sql/#{0}#'\
.format(self.manager.version)
# We need parent's name eg table name and schema name
SQL = render_template("/".join([self.template_path,
@ -505,7 +519,8 @@ class IndexConstraintView(PGChildNodeView):
"""
self.manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
self.conn = self.manager.connection(did=did)
self.template_path = 'index_constraint/sql'
self.template_path = 'index_constraint/sql/#{0}#'\
.format(self.manager.version)
# We need parent's name eg table name and schema name
SQL = render_template("/".join([self.template_path,
@ -935,7 +950,7 @@ class IndexConstraintView(PGChildNodeView):
sql = render_template(
"/".join([self.template_path, 'get_constraint_cols.sql']),
cid=cid, colcnt=data['indnatts'])
cid=cid, colcnt=data['col_count'])
status, res = self.conn.execute_dict(sql)
@ -948,6 +963,18 @@ class IndexConstraintView(PGChildNodeView):
data['columns'] = columns
# Add Include details of the index supported for PG-11+
if self.manager.version >= 110000:
sql = render_template(
"/".join([self.template_path, 'get_constraint_include.sql']),
cid=cid)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
data['include'] = [col['colname'] for col in res['rows']]
SQL = render_template(
"/".join([self.template_path, 'create.sql']),
data=data,

View File

@ -100,6 +100,7 @@ define('pgadmin.node.primary_key', [
condeferrable: undefined,
condeferred: undefined,
columns: [],
include: [],
},
// Define the schema for the index constraint node
@ -400,6 +401,103 @@ define('pgadmin.node.primary_key', [
}
},
},{
id: 'include', label: gettext('Include columns'),
type: 'array', group: gettext('Definition'),
editable: false,
canDelete: true, canAdd: true, mode: ['properties', 'create', 'edit'],
visible: function(m) {
/* In table properties, m.node_info is not available */
m = m.top;
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
&& !_.isUndefined(m.node_info.server.version) &&
m.node_info.server.version >= 110000)
return true;
return false;
},
control: Backform.MultiSelectAjaxControl.extend({
defaults: _.extend(
{},
Backform.NodeListByNameControl.prototype.defaults,
{
select2: {
allowClear: false,
width: 'style',
multiple: true,
placeholder: gettext('Select the column(s)'),
},
}
),
initialize: function() {
// Here we will decide if we need to call URL
// Or fetch the data from parent columns collection
var self = this;
if(this.model.handler) {
Backform.Select2Control.prototype.initialize.apply(this, arguments);
// Do not listen for any event(s) for existing constraint.
if (_.isUndefined(self.model.get('oid'))) {
var tableCols = self.model.top.get('columns');
self.listenTo(tableCols, 'remove' , self.resetColOptions);
self.listenTo(tableCols, 'change:name', self.resetColOptions);
}
self.custom_options();
} else {
Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments);
}
},
resetColOptions: function() {
var self = this;
setTimeout(function () {
self.custom_options();
self.render.apply(self);
}, 50);
},
custom_options: function() {
// We will add all the columns entered by user in table model
var columns = this.model.top.get('columns'),
added_columns_from_tables = [];
if (columns.length > 0) {
_.each(columns.models, function(m) {
var col = m.get('name');
if(!_.isUndefined(col) && !_.isNull(col)) {
added_columns_from_tables.push(
{label: col, value: col, image:'icon-column'}
);
}
});
}
// Set the values in to options so that user can select
this.field.set('options', added_columns_from_tables);
},
}),
deps: ['index'], node: 'column',
disabled: function(m) {
// If we are in table edit mode then
if (_.has(m, 'top') && !_.isUndefined(m.top)
&& !m.top.isNew()) {
// If OID is undefined then user is trying to add
// new constraint which should be allowed for Unique
return !_.isUndefined(m.get('oid'));
}
// We can't update columns of existing index constraint.
if (!m.isNew()) {
return true;
}
// Disable if index is selected.
var index = m.get('index');
if(_.isUndefined(index) || index == '') {
return false;
} else {
var col = m.get('columns');
col.reset();
return true;
}
},
},{
id: 'spcname', label: gettext('Tablespace'),
type: 'text', group: gettext('Definition'),
control: 'node-list-by-name', node: 'tablespace',

View File

@ -86,6 +86,7 @@ define('pgadmin.node.unique_constraint', [
condeferrable: undefined,
condeferred: undefined,
columns: [],
include: [],
},
// Define the schema for the index constraint node
@ -386,6 +387,103 @@ define('pgadmin.node.unique_constraint', [
}
},
},{
id: 'include', label: gettext('Include columns'),
type: 'array', group: gettext('Definition'),
editable: false,
canDelete: true, canAdd: true, mode: ['properties', 'create', 'edit'],
visible: function(m) {
/* In table properties, m.node_info is not available */
m = m.top;
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
&& !_.isUndefined(m.node_info.server.version) &&
m.node_info.server.version >= 110000)
return true;
return false;
},
control: Backform.MultiSelectAjaxControl.extend({
defaults: _.extend(
{},
Backform.NodeListByNameControl.prototype.defaults,
{
select2: {
allowClear: false,
width: 'style',
multiple: true,
placeholder: gettext('Select the column(s)'),
},
}
),
initialize: function() {
// Here we will decide if we need to call URL
// Or fetch the data from parent columns collection
var self = this;
if(this.model.handler) {
Backform.Select2Control.prototype.initialize.apply(this, arguments);
// Do not listen for any event(s) for existing constraint.
if (_.isUndefined(self.model.get('oid'))) {
var tableCols = self.model.top.get('columns');
self.listenTo(tableCols, 'remove' , self.resetColOptions);
self.listenTo(tableCols, 'change:name', self.resetColOptions);
}
self.custom_options();
} else {
Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments);
}
},
resetColOptions: function() {
var self = this;
setTimeout(function () {
self.custom_options();
self.render.apply(self);
}, 50);
},
custom_options: function() {
// We will add all the columns entered by user in table model
var columns = this.model.top.get('columns'),
added_columns_from_tables = [];
if (columns.length > 0) {
_.each(columns.models, function(m) {
var col = m.get('name');
if(!_.isUndefined(col) && !_.isNull(col)) {
added_columns_from_tables.push(
{label: col, value: col, image:'icon-column'}
);
}
});
}
// Set the values in to options so that user can select
this.field.set('options', added_columns_from_tables);
},
}),
deps: ['index'], node: 'column',
disabled: function(m) {
// If we are in table edit mode then
if (_.has(m, 'top') && !_.isUndefined(m.top)
&& !m.top.isNew()) {
// If OID is undefined then user is trying to add
// new constraint which should be allowed for Unique
return !_.isUndefined(m.get('oid'));
}
// We can't update columns of existing index constraint.
if (!m.isNew()) {
return true;
}
// Disable if index is selected.
var index = m.get('index');
if(_.isUndefined(index) || index == '') {
return false;
} else {
var col = m.get('columns');
col.reset();
return true;
}
},
},{
id: 'spcname', label: gettext('Tablespace'),
type: 'text', group: gettext('Definition'),
control: 'node-list-by-name', node: 'tablespace',

View File

@ -527,10 +527,35 @@ class IndexesView(PGChildNodeView):
# Push as collection
data['columns'] = columns
# Push as string
data['cols'] = ', '.join(cols)
data['columns_csv'] = ', '.join(cols)
return data
def _include_details(self, idx, data, mode='properties'):
"""
This functional will fetch list of include details for index
supported with Postgres 11+
Args:
idx: Index OID
data: Properties data
Returns:
Updated properties data with include details
"""
SQL = render_template(
"/".join([self.template_path, 'include_details.sql']), idx=idx
)
status, rset = self.conn.execute_2darray(SQL)
if not status:
return internal_server_error(errormsg=rset)
# Push as collection
data['include'] = [col['colname'] for col in rset['rows']]
return data
@check_precondition
def properties(self, gid, sid, did, scid, tid, idx):
"""
@ -568,6 +593,10 @@ class IndexesView(PGChildNodeView):
# Add column details for current index
data = self._column_details(idx, data)
# Add Include details of the index
if self.manager.version >= 110000:
data = self._include_details(idx, data)
return ajax_response(
response=data,
status=200
@ -905,6 +934,10 @@ class IndexesView(PGChildNodeView):
# Add column details for current index
data = self._column_details(idx, data, 'create')
# Add Include details of the index
if self.manager.version >= 110000:
data = self._include_details(idx, data, 'create')
SQL, name = self.get_sql(did, scid, tid, None, data)
if not isinstance(SQL, (str, unicode)):
return SQL

View File

@ -226,6 +226,7 @@ define('pgadmin.node.index', [
hasDepends: true,
hasStatistics: true,
statsPrettifyFields: ['Size', 'Index size'],
width: '45%',
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
@ -331,9 +332,47 @@ define('pgadmin.node.index', [
},
}),
},{
id: 'cols', label: gettext('Columns'), cell: 'string',
id: 'columns_csv', label: gettext('Columns'), cell: 'string',
type: 'text', disabled: 'inSchema', mode: ['properties'],
group: gettext('Definition'),
},{
id: 'include', label: gettext('Include columns'),
type: 'array', group: gettext('Definition'),
editable: false,
canDelete: true, canAdd: true, mode: ['properties'],
disabled: 'inSchemaWithModelCheck',
visible: function(m) {
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
&& !_.isUndefined(m.node_info.server.version) &&
m.node_info.server.version >= 110000)
return true;
return false;
},
control: Backform.MultiSelectAjaxControl.extend({
defaults: _.extend(
{},
Backform.NodeListByNameControl.prototype.defaults,
{
select2: {
allowClear: false,
width: 'style',
multiple: true,
placeholder: gettext('Select the column(s)'),
},
}
),
}),
transform : function(data){
var res = [];
if (data && _.isArray(data)) {
_.each(data, function(d) {
res.push({label: d.label, value: d.label, image:'icon-column'});
});
}
return res;
},
node:'column',
},{
id: 'fillfactor', label: gettext('Fill factor'), cell: 'string',
type: 'int', disabled: 'inSchema', mode: ['create', 'edit', 'properties'],
@ -387,6 +426,44 @@ define('pgadmin.node.index', [
},
control: 'unique-col-collection', uniqueCol : ['colname'],
columns: ['colname', 'op_class', 'sort_order', 'nulls', 'collspcname'],
},{
id: 'include', label: gettext('Include columns'),
type: 'array', group: gettext('Definition'),
editable: false,
canDelete: true, canAdd: true, mode: ['edit', 'create'],
disabled: 'inSchemaWithModelCheck',
visible: function(m) {
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
&& !_.isUndefined(m.node_info.server.version) &&
m.node_info.server.version >= 110000)
return true;
return false;
},
control: Backform.MultiSelectAjaxControl.extend({
defaults: _.extend(
{},
Backform.NodeListByNameControl.prototype.defaults,
{
select2: {
allowClear: false,
width: 'style',
multiple: true,
placeholder: gettext('Select the column(s)'),
},
}
),
}),
transform : function(data){
var res = [];
if (data && _.isArray(data)) {
_.each(data, function(d) {
res.push({label: d.label, value: d.label, image:'icon-column'});
});
}
return res;
},
node:'column',
},{
id: 'description', label: gettext('Comment'), cell: 'string',
type: 'multiline', mode: ['properties', 'create', 'edit'],

View File

@ -55,7 +55,9 @@ class IndexesAddTestCase(BaseTestGenerator):
"spcname": "pg_default",
"amname": "btree",
"columns": [
{"colname": "id", "sort_order": False, "nulls": False}]}
{"colname": "id", "sort_order": False, "nulls": False}],
"include": ["name"]
}
response = self.tester.post(
self.url + str(utils.SERVER_GROUP) + '/' +
str(self.server_id) + '/' + str(self.db_id) +

View File

@ -0,0 +1,21 @@
ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }}
ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} (
{% for col in data.columns %}{% if loop.index != 1 %},
{% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %})
{% if data.include|length > 0 %}
INCLUDE({% for col in data.include %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(col)}}{% endfor %}){% endif %}
{% if data.fillfactor %}
WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %}
USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %}
{% if data.condeferrable %}
DEFERRABLE{% if data.condeferred %}
INITIALLY DEFERRED{% endif%}
{% endif%}{% if data.constraint %} WHERE ({{data.constraint}}){% endif%};
{% if data.comment and data.name %}
COMMENT ON CONSTRAINT {{ conn|qtIdent(data.name) }} ON {{ conn|qtIdent(data.schema, data.table) }}
IS {{ data.comment|qtLiteral }};
{% endif %}

View File

@ -0,0 +1,16 @@
-- pg_get_indexdef did not support INCLUDE columns
SELECT a.attname as colname
FROM (
SELECT
i.indnkeyatts,
i.indrelid,
unnest(indkey) AS table_colnum,
unnest(ARRAY(SELECT generate_series(1, i.indnatts) AS n)) attnum
FROM
pg_index i
WHERE i.indexrelid = {{cid}}::OID
) i JOIN pg_attribute a
ON (a.attrelid = i.indrelid AND i.table_colnum = a.attnum)
WHERE i.attnum > i.indnkeyatts
ORDER BY i.attnum

View File

@ -0,0 +1,34 @@
SELECT cls.oid,
cls.relname as name,
indnkeyatts as col_count,
amname,
CASE WHEN length(spcname) > 0 THEN spcname ELSE
(SELECT sp.spcname FROM pg_database dtb
JOIN pg_tablespace sp ON dtb.dattablespace=sp.oid
WHERE dtb.oid = {{ did }}::oid)
END as spcname,
CASE contype
WHEN 'p' THEN desp.description
WHEN 'u' THEN desp.description
WHEN 'x' THEN desp.description
ELSE des.description
END AS comment,
condeferrable,
condeferred,
substring(array_to_string(cls.reloptions, ',') from 'fillfactor=([0-9]*)') AS fillfactor
FROM pg_index idx
JOIN pg_class cls ON cls.oid=indexrelid
JOIN pg_class tab ON tab.oid=indrelid
LEFT OUTER JOIN pg_tablespace ta on ta.oid=cls.reltablespace
JOIN pg_namespace n ON n.oid=tab.relnamespace
JOIN pg_am am ON am.oid=cls.relam
LEFT JOIN pg_depend dep ON (dep.classid = cls.tableoid AND dep.objid = cls.oid AND dep.refobjsubid = '0' AND dep.refclassid=(SELECT oid FROM pg_class WHERE relname='pg_constraint') AND dep.deptype='i')
LEFT OUTER JOIN pg_constraint con ON (con.tableoid = dep.refclassid AND con.oid = dep.refobjid)
LEFT OUTER JOIN pg_description des ON (des.objoid=cls.oid AND des.classoid='pg_class'::regclass)
LEFT OUTER JOIN pg_description desp ON (desp.objoid=con.oid AND desp.objsubid = 0 AND desp.classoid='pg_constraint'::regclass)
WHERE indrelid = {{tid}}::oid
{% if cid %}
AND cls.oid = {{cid}}::oid
{% endif %}
AND contype='x'
ORDER BY cls.relname

View File

@ -1,6 +1,6 @@
SELECT cls.oid,
cls.relname as name,
indnatts,
indnatts as col_count,
amname,
CASE WHEN length(spcname) > 0 THEN spcname ELSE
(SELECT sp.spcname FROM pg_database dtb
@ -31,4 +31,4 @@ WHERE indrelid = {{tid}}::oid
AND cls.oid = {{cid}}::oid
{% endif %}
AND contype='x'
ORDER BY cls.relname
ORDER BY cls.relname

View File

@ -1,4 +1,4 @@
SELECT cls.oid, cls.relname as idxname, indnatts
SELECT cls.oid, cls.relname as idxname, indnatts as col_count
FROM pg_index idx
JOIN pg_class cls ON cls.oid=indexrelid
LEFT JOIN pg_depend dep ON (dep.classid = cls.tableoid AND dep.objid = cls.oid AND dep.refobjsubid = '0' AND dep.refclassid=(SELECT oid FROM pg_class WHERE relname='pg_constraint') AND dep.deptype='i')
@ -34,4 +34,4 @@ SELECT cls.oid, cls.relname as idxname, indnatts
LEFT JOIN pg_depend dep ON (dep.classid = cls.tableoid AND dep.objid = cls.oid AND dep.refobjsubid = '0' AND dep.refclassid=(SELECT oid FROM pg_class WHERE relname='pg_constraint') AND dep.deptype='i')
LEFT OUTER JOIN pg_constraint con ON (con.tableoid = dep.refclassid AND con.oid = dep.refobjid)
WHERE idx.indrelid = {{tid}}::oid
AND conname IS NULL
AND conname IS NULL

View File

@ -0,0 +1,31 @@
SELECT
i.indexrelid,
CASE i.indoption[i.attnum - 1]
WHEN 0 THEN ARRAY['ASC', 'NULLS LAST']
WHEN 1 THEN ARRAY['DESC', 'NULLS FIRST']
WHEN 2 THEN ARRAY['ASC', 'NULLS FIRST']
WHEN 3 THEN ARRAY['DESC', 'NULLS ']
ELSE ARRAY['UNKNOWN OPTION' || i.indoption[i.attnum - 1], '']
END::text[] AS options,
i.attnum,
pg_get_indexdef(i.indexrelid, i.attnum, true) as attdef,
CASE WHEN (o.opcdefault = FALSE) THEN o.opcname ELSE null END AS opcname,
op.oprname AS oprname,
CASE WHEN length(nspc.nspname) > 0 AND length(coll.collname) > 0 THEN
concat(quote_ident(nspc.nspname), '.', quote_ident(coll.collname))
ELSE '' END AS collnspname
FROM (
SELECT
indexrelid, i.indoption, i.indclass,
unnest(ARRAY(SELECT generate_series(1, i.indnkeyatts) AS n)) AS attnum
FROM
pg_index i
WHERE i.indexrelid = {{idx}}::OID
) i
LEFT JOIN pg_opclass o ON (o.oid = i.indclass[i.attnum - 1])
LEFT OUTER JOIN pg_constraint c ON (c.conindid = i.indexrelid)
LEFT OUTER JOIN pg_operator op ON (op.oid = c.conexclop[i.attnum])
LEFT JOIN pg_attribute a ON (a.attrelid = i.indexrelid AND a.attnum = i.attnum)
LEFT OUTER JOIN pg_collation coll ON a.attcollation=coll.oid
LEFT OUTER JOIN pg_namespace nspc ON coll.collnamespace=nspc.oid
ORDER BY i.attnum;

View File

@ -0,0 +1,25 @@
CREATE {% if data.indisunique %}UNIQUE {% endif %}INDEX {% if data.isconcurrent %}CONCURRENTLY {% endif %}{{conn|qtIdent(data.name)}}
ON {{conn|qtIdent(data.schema, data.table)}} {% if data.amname %}USING {{conn|qtIdent(data.amname)}}{% endif %}
{% if mode == 'create' %}
({% for c in data.columns %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(c.colname)}}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.op_class %}
{{c.op_class}}{% endif %}{% if data.amname is defined %}{% if c.sort_order is defined and c.is_sort_nulls_applicable %}{% if c.sort_order %} DESC{% else %} ASC{% endif %}{% endif %}{% if c.nulls is defined and c.is_sort_nulls_applicable %} NULLS {% if c.nulls %}
FIRST{% else %}LAST{% endif %}{% endif %}{% endif %}{% endfor %})
{% if data.include|length > 0 %}
INCLUDE({% for col in data.include %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(col)}}{% endfor %})
{% endif %}
{% else %}
{## We will get indented data from postgres for column ##}
({% for c in data.columns %}{% if loop.index != 1 %}, {% endif %}{{c.colname}}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.op_class %}
{{c.op_class}}{% endif %}{% if c.sort_order is defined %}{% if c.sort_order %} DESC{% else %} ASC{% endif %}{% endif %}{% if c.nulls is defined %} NULLS {% if c.nulls %}
FIRST{% else %}LAST{% endif %}{% endif %}{% endfor %})
{% if data.include|length > 0 %}
INCLUDE({% for col in data.include %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(col)}}{% endfor %})
{% endif %}
{% endif %}
{% if data.fillfactor %}
WITH (FILLFACTOR={{data.fillfactor}})
{% endif %}{% if data.spcname %}
TABLESPACE {{conn|qtIdent(data.spcname)}}{% endif %}{% if data.indconstraint %}
WHERE {{data.indconstraint}}
{% endif %};

View File

@ -0,0 +1,16 @@
-- pg_get_indexdef did not support INCLUDE columns
SELECT a.attname as colname
FROM (
SELECT
i.indnkeyatts,
i.indrelid,
unnest(indkey) AS table_colnum,
unnest(ARRAY(SELECT generate_series(1, i.indnatts) AS n)) attnum
FROM
pg_index i
WHERE i.indexrelid = {{idx}}::OID
) i JOIN pg_attribute a
ON (a.attrelid = i.indrelid AND i.table_colnum = a.attnum)
WHERE i.attnum > i.indnkeyatts
ORDER BY i.attnum

View File

@ -0,0 +1,20 @@
ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }}
ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} {{constraint_name}} {% if data.index %}USING INDEX {{ conn|qtIdent(data.index) }}{% else %}
({% for columnobj in data.columns %}{% if loop.index != 1 %}
, {% endif %}{{ conn|qtIdent(columnobj.column)}}{% endfor %})
{% if data.include|length > 0 %}
INCLUDE({% for col in data.include %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(col)}}{% endfor %}){% endif %}
{% if data.fillfactor %}
WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %}
USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %}{% endif %}{% if data.condeferrable %}
DEFERRABLE{% if data.condeferred %}
INITIALLY DEFERRED{% endif%}
{% endif%};
{% if data.comment and data.name %}
COMMENT ON CONSTRAINT {{ conn|qtIdent(data.name) }} ON {{ conn|qtIdent(data.schema, data.table) }}
IS {{ data.comment|qtLiteral }};
{% endif %}

View File

@ -0,0 +1,16 @@
-- pg_get_indexdef did not support INCLUDE columns
SELECT a.attname as colname
FROM (
SELECT
i.indnkeyatts,
i.indrelid,
unnest(indkey) AS table_colnum,
unnest(ARRAY(SELECT generate_series(1, i.indnatts) AS n)) attnum
FROM
pg_index i
WHERE i.indexrelid = {{cid}}::OID
) i JOIN pg_attribute a
ON (a.attrelid = i.indrelid AND i.table_colnum = a.attnum)
WHERE i.attnum > i.indnkeyatts
ORDER BY i.attnum

View File

@ -0,0 +1,33 @@
SELECT cls.oid,
cls.relname as name,
indnkeyatts as col_count,
CASE WHEN length(spcname) > 0 THEN spcname ELSE
(SELECT sp.spcname FROM pg_database dtb
JOIN pg_tablespace sp ON dtb.dattablespace=sp.oid
WHERE dtb.oid = {{ did }}::oid)
END as spcname,
CASE contype
WHEN 'p' THEN desp.description
WHEN 'u' THEN desp.description
WHEN 'x' THEN desp.description
ELSE des.description
END AS comment,
condeferrable,
condeferred,
substring(array_to_string(cls.reloptions, ',') from 'fillfactor=([0-9]*)') AS fillfactor
FROM pg_index idx
JOIN pg_class cls ON cls.oid=indexrelid
JOIN pg_class tab ON tab.oid=indrelid
LEFT OUTER JOIN pg_tablespace ta on ta.oid=cls.reltablespace
JOIN pg_namespace n ON n.oid=tab.relnamespace
JOIN pg_am am ON am.oid=cls.relam
LEFT JOIN pg_depend dep ON (dep.classid = cls.tableoid AND dep.objid = cls.oid AND dep.refobjsubid = '0' AND dep.refclassid=(SELECT oid FROM pg_class WHERE relname='pg_constraint') AND dep.deptype='i')
LEFT OUTER JOIN pg_constraint con ON (con.tableoid = dep.refclassid AND con.oid = dep.refobjid)
LEFT OUTER JOIN pg_description des ON (des.objoid=cls.oid AND des.classoid='pg_class'::regclass)
LEFT OUTER JOIN pg_description desp ON (desp.objoid=con.oid AND desp.objsubid = 0 AND desp.classoid='pg_constraint'::regclass)
WHERE indrelid = {{tid}}::oid
{% if cid %}
AND cls.oid = {{cid}}::oid
{% endif %}
AND contype='{{constraint_type}}'
ORDER BY cls.relname

View File

@ -1,6 +1,6 @@
SELECT cls.oid,
cls.relname as name,
indnatts,
indnatts as col_count,
CASE WHEN length(spcname) > 0 THEN spcname ELSE
(SELECT sp.spcname FROM pg_database dtb
JOIN pg_tablespace sp ON dtb.dattablespace=sp.oid
@ -30,4 +30,4 @@ WHERE indrelid = {{tid}}::oid
AND cls.oid = {{cid}}::oid
{% endif %}
AND contype='{{constraint_type}}'
ORDER BY cls.relname
ORDER BY cls.relname

View File

@ -7,7 +7,10 @@
{% if data.columns|length > 0 %}
{% if data.name %}CONSTRAINT {{conn|qtIdent(data.name)}} {% endif %}PRIMARY KEY ({% for c in data.columns%}
{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(c.column)}}{% endfor %}){% if data.fillfactor %}
{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(c.column)}}{% endfor %}){% if data.include|length > 0 %}
INCLUDE({% for col in data.include %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(col)}}{% endfor %}){% endif %}
{% if data.fillfactor %}
WITH (FILLFACTOR={{data.fillfactor}}){% endif %}
{% if data.spcname and data.spcname != "pg_default" %}
@ -23,7 +26,10 @@
{% if data.columns|length > 0 %}{% if loop.index !=1 %},{% endif %}
{% if data.name %}CONSTRAINT {{conn|qtIdent(data.name)}} {% endif %}UNIQUE ({% for c in data.columns%}
{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(c.column)}}{% endfor %}){% if data.fillfactor %}
{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(c.column)}}{% endfor %})
{% if data.include|length > 0 %}
INCLUDE({% for col in data.include %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(col)}}{% endfor %}){% endif %}
{% if data.fillfactor %}
WITH (FILLFACTOR={{data.fillfactor}}){% endif %}
{% if data.spcname and data.spcname != "pg_default" %}
@ -78,9 +84,11 @@
{% if data.name %}CONSTRAINT {{ conn|qtIdent(data.name) }} {% endif%}EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} (
{% for col in data.columns %}{% if loop.index != 1 %},
{% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %}
{% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %})
{% if data.include|length > 0 %}
INCLUDE({% for col in data.include %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(col)}}{% endfor %})
{% endif %}{% if data.fillfactor %}
WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %}
USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %}
{% if data.condeferrable %}
@ -99,4 +107,4 @@ COMMENT ON CONSTRAINT {{ conn|qtIdent(d.name) }} ON {{ conn|qtIdent(schema, tabl
IS {{ d.comment|qtLiteral }};
{% endif %}
{% endfor %}
{%- endmacro %}
{%- endmacro %}

View File

@ -127,7 +127,8 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
'exclusion_constraint/sql', server_type, ver)
# Template for PK & Unique constraint node
self.index_constraint_template_path = 'index_constraint/sql'
self.index_constraint_template_path = 'index_constraint/sql/#{0}#'\
.format(ver)
# Template for foreign key constraint node
self.foreign_key_template_path = compile_template_path(
@ -368,7 +369,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
"/".join([self.index_constraint_template_path,
'get_constraint_cols.sql']),
cid=row['oid'],
colcnt=row['indnatts'])
colcnt=row['col_count'])
status, res = self.conn.execute_dict(sql)
if not status:
@ -380,6 +381,19 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
result['columns'] = columns
# INCLUDE clause in index is supported from PG-11+
if self.manager.version >= 110000:
sql = render_template(
"/".join([self.index_constraint_template_path,
'get_constraint_include.sql']),
cid=row['oid'])
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
result['include'] = [col['colname'] for col in res['rows']]
# If not exists then create list and/or append into
# existing list [ Adding into main data dict]
data.setdefault(index_constraints[ctype], []).append(result)
@ -513,7 +527,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
[self.exclusion_constraint_template_path,
'get_constraint_cols.sql']),
cid=ex['oid'],
colcnt=ex['indnatts'])
colcnt=ex['col_count'])
status, res = self.conn.execute_dict(sql)
@ -538,6 +552,20 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
})
ex['columns'] = columns
# INCLUDE clause in index is supported from PG-11+
if self.manager.version >= 110000:
sql = render_template(
"/".join([self.exclusion_constraint_template_path,
'get_constraint_include.sql']),
cid=ex['oid'])
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
ex['include'] = [col['colname'] for col in res['rows']]
# If not exists then create list and/or append into
# existing list [ Adding into main data dict]
data.setdefault('exclude_constraint', []).append(ex)
@ -962,6 +990,18 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
# Push as string
data['cols'] = ', '.join(cols)
if self.manager.version >= 110000:
SQL = render_template(
"/".join([self.index_template_path,
'include_details.sql']),
idx=row['oid'])
status, res = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=res)
data['include'] = [col['colname'] for col in res['rows']]
sql_header = u"\n-- Index: {0}\n\n-- ".format(data['name'])
sql_header += render_template("/".join([self.index_template_path,
@ -2389,7 +2429,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
sql = render_template(
"/".join([self.foreign_key_template_path, 'get_cols.sql']),
cid=costrnt['oid'],
colcnt=costrnt['indnatts'])
colcnt=costrnt['col_count'])
status, rest = self.conn.execute_dict(sql)
if not status:

View File

@ -12,6 +12,7 @@ import codecs
import os
import pickle
import random
import sys
import simplejson as json
from flask import Response, url_for, render_template, session, request, \
@ -50,6 +51,11 @@ try:
except ImportError:
from urllib.parse import unquote
if sys.version_info[0:2] <= (2, 7):
IS_PY2 = True
else:
IS_PY2 = False
class SqlEditorModule(PgAdminModule):
"""
@ -310,8 +316,10 @@ def extract_sql_from_network_parameters(request_data, request_arguments,
request_form_data):
if request_data:
sql_parameters = json.loads(request_data, encoding='utf-8')
if type(sql_parameters) is str:
return dict(sql=sql_parameters, explain_plan=None)
if (IS_PY2 and type(sql_parameters) is unicode) \
or type(sql_parameters) is str:
return dict(sql=str(sql_parameters), explain_plan=None)
return sql_parameters
else:
return request_arguments or request_form_data