Added option to create unique index with nulls not distinct. #6368

This commit is contained in:
Pravesh Sharma 2023-06-19 15:09:48 +05:30 committed by GitHub
parent 557f33c4f9
commit 36949aef99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 457 additions and 5 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -54,6 +54,8 @@ Use the fields in the *Definition* tab to define the index:
* Move the *Unique?* switch to the *Yes* position to check for duplicate values * Move the *Unique?* switch to the *Yes* position to check for duplicate values
in the table when the index is created and when data is added. The default is in the table when the index is created and when data is added. The default is
*No*. *No*.
* Move the *NULLs not distinct?* switch to the *Yes* position to treat null values as not distinct. The default is
*No*. This option is available only on PostgreSQL 15 and above.
* Move the *Clustered?* switch to the *Yes* position to instruct the server to * Move the *Clustered?* switch to the *Yes* position to instruct the server to
cluster the table. cluster the table.
* Move the *Concurrent build?* switch to the *Yes* position to build the index * Move the *Concurrent build?* switch to the *Yes* position to build the index

View File

@ -51,6 +51,8 @@ Use the fields in the *Definition* tab to define the unique constraint:
* If enabled, move the *Deferred?* switch to the *Yes* position to specify the * If enabled, move the *Deferred?* switch to the *Yes* position to specify the
timing of the constraint is deferred to the end of the statement. The default timing of the constraint is deferred to the end of the statement. The default
is *No*. is *No*.
* Move the *NULLs not distinct?* switch to the *Yes* position to treat null values as not distinct. The default is
*No*. This option is available only on PostgreSQL 15 and above.
Click the *SQL* tab to continue. Click the *SQL* tab to continue.

View File

@ -277,7 +277,7 @@ class IndexesView(PGChildNodeView, SchemaDiffObjectCompare):
This function will return list of collation available This function will return list of collation available
via AJAX response via AJAX response
""" """
res = [{'label': '', 'value': ''}] res = []
try: try:
SQL = render_template( SQL = render_template(
"/".join([self.template_path, 'get_collations.sql']) "/".join([self.template_path, 'get_collations.sql'])
@ -305,7 +305,7 @@ class IndexesView(PGChildNodeView, SchemaDiffObjectCompare):
This function will return list of access methods available This function will return list of access methods available
via AJAX response via AJAX response
""" """
res = [{'label': '', 'value': ''}] res = []
try: try:
SQL = render_template("/".join([self.template_path, 'get_am.sql'])) SQL = render_template("/".join([self.template_path, 'get_am.sql']))
status, rset = self.conn.execute_2darray(SQL) status, rset = self.conn.execute_2darray(SQL)
@ -349,7 +349,7 @@ class IndexesView(PGChildNodeView, SchemaDiffObjectCompare):
if not status: if not status:
return internal_server_error(errormsg=res) return internal_server_error(errormsg=res)
op_class_list = [{'label': '', 'value': ''}] op_class_list = []
for r in result['rows']: for r in result['rows']:
op_class_list.append({'label': r['opcname'], op_class_list.append({'label': r['opcname'],

View File

@ -359,10 +359,32 @@ export default class IndexSchema extends BaseUISchema {
min: 10, max:100, group: gettext('Definition'), min: 10, max:100, group: gettext('Definition'),
},{ },{
id: 'indisunique', label: gettext('Unique?'), cell: 'string', id: 'indisunique', label: gettext('Unique?'), cell: 'string',
type: 'switch', disabled: () => inSchema(indexSchemaObj.node_info), type: 'switch', deps:['amname'], disabled: (state) => {
return state.amname !== 'btree' || inSchema(indexSchemaObj.node_info);
},
readonly: function (state) { readonly: function (state) {
return !indexSchemaObj.isNew(state); return !indexSchemaObj.isNew(state);
}, },
depChange: (state) => {
if (state.amname !== 'btree') {
return {indisunique:false};
}
},
group: gettext('Definition'),
},{
id: 'indnullsnotdistinct', label: gettext('NULLs not distinct?'), cell: 'string',
type: 'switch', deps:['indisunique', 'amname'], disabled: (state) => {
return !state.indisunique || inSchema(indexSchemaObj.node_info);
},
readonly: function (state) {
return !indexSchemaObj.isNew(state);
},
depChange: (state) => {
if (!state.indisunique) {
return {indnullsnotdistinct:false};
}
},
min_version: 150000,
group: gettext('Definition'), group: gettext('Definition'),
},{ },{
id: 'indisclustered', label: gettext('Clustered?'), cell: 'string', id: 'indisclustered', label: gettext('Clustered?'), cell: 'string',

View File

@ -0,0 +1,14 @@
-- Index: Idx_$%{}[]()&*^!@"'`\/#
-- DROP INDEX IF EXISTS public."Idx_$%{}[]()&*^!@""'`\/#";
CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id ASC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS FIRST)
NULLS NOT DISTINCT
WITH (FILLFACTOR=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
IS 'Test Comment';

View File

@ -0,0 +1,10 @@
CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id ASC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS FIRST)
NULLS NOT DISTINCT
WITH (FILLFACTOR=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
IS 'Test Comment';

View File

@ -0,0 +1,14 @@
-- Index: Idx_$%{}[]()&*^!@"'`\/#
-- DROP INDEX IF EXISTS public."Idx_$%{}[]()&*^!@""'`\/#";
CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id ASC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS LAST)
NULLS NOT DISTINCT
WITH (FILLFACTOR=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
IS 'Test Comment';

View File

@ -0,0 +1,10 @@
CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id ASC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS LAST)
NULLS NOT DISTINCT
WITH (FILLFACTOR=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
IS 'Test Comment';

View File

@ -0,0 +1,9 @@
-- Index: Idx_$%{}[]()&*^!@"'`\/#
-- DROP INDEX IF EXISTS public."Idx_$%{}[]()&*^!@""'`\/#";
CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
NULLS NOT DISTINCT
TABLESPACE pg_default;

View File

@ -0,0 +1,5 @@
CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
NULLS NOT DISTINCT
TABLESPACE pg_default;

View File

@ -0,0 +1,14 @@
-- Index: Idx_$%{}[]()&*^!@"'`\/#
-- DROP INDEX IF EXISTS public."Idx_$%{}[]()&*^!@""'`\/#";
CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id DESC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS LAST)
NULLS NOT DISTINCT
WITH (FILLFACTOR=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
IS 'Test Comment';

View File

@ -0,0 +1,10 @@
CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id DESC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS LAST)
NULLS NOT DISTINCT
WITH (FILLFACTOR=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
IS 'Test Comment';

View File

@ -0,0 +1,207 @@
{
"scenarios": [
{
"type": "create",
"name": "Create Table for indexes",
"endpoint": "NODE-table.obj",
"sql_endpoint": "NODE-table.sql_id",
"data": {
"name": "test_table_for_indexes",
"columns": [{
"name": "id",
"cltype": "bigint",
"is_primary_key": true
}, {
"name": "name",
"cltype": "text"
}],
"is_partitioned": false,
"spcname": "pg_default",
"schema": "public"
},
"store_object_id": true
},
{
"type": "create",
"name": "Create btree index with ASC and NULLS LAST -- 15 Plus",
"endpoint": "NODE-index.obj",
"sql_endpoint": "NODE-index.sql_id",
"msql_endpoint": "NODE-index.msql",
"data": {
"name":"Idx_$%{}[]()&*^!@\"'`\\/#",
"spcname":"pg_default",
"amname":"btree",
"columns":[{
"colname":"id",
"collspcname":"",
"op_class":"",
"sort_order":false,
"nulls":false,
"is_sort_nulls_applicable":true
}, {
"colname":"name",
"collspcname":"pg_catalog.\"POSIX\"",
"op_class":"text_pattern_ops",
"sort_order":false,
"nulls":false,
"is_sort_nulls_applicable":true
}],
"description":"Test Comment",
"fillfactor":"10",
"indisunique":true,
"indnullsnotdistinct":true,
"indisclustered":false,
"isconcurrent":false,
"indconstraint":"id < 100"
},
"expected_sql_file": "create_btree_asc_null_last.sql",
"expected_msql_file": "create_btree_asc_null_last_msql.sql"
},
{
"type": "delete",
"name": "Drop index -- 15 Plus",
"endpoint": "NODE-index.delete_id",
"data": {
"name": "Idx_$%{}[]()&*^!@\"'`\\/#"
}
},
{
"type": "create",
"name": "Create btree index with ASC and NULLS FIRST -- 15 Plus",
"endpoint": "NODE-index.obj",
"sql_endpoint": "NODE-index.sql_id",
"msql_endpoint": "NODE-index.msql",
"data": {
"name":"Idx_$%{}[]()&*^!@\"'`\\/#",
"spcname":"pg_default",
"amname":"btree",
"columns":[{
"colname":"id",
"collspcname":"",
"op_class":"",
"sort_order":false,
"nulls":true,
"is_sort_nulls_applicable":true
}, {
"colname":"name",
"collspcname":"pg_catalog.\"POSIX\"",
"op_class":"text_pattern_ops",
"sort_order":false,
"nulls":true,
"is_sort_nulls_applicable":true
}],
"description":"Test Comment",
"fillfactor":"10",
"indisunique":true,
"indnullsnotdistinct":true,
"indisclustered":false,
"isconcurrent":false,
"indconstraint":"id < 100"
},
"expected_sql_file": "create_btree_asc_null_first.sql",
"expected_msql_file": "create_btree_asc_null_first_msql.sql"
},
{
"type": "delete",
"name": "Drop index -- 15 Plus",
"endpoint": "NODE-index.delete_id",
"data": {
"name": "Idx_$%{}[]()&*^!@\"'`\\/#"
}
},
{
"type": "create",
"name": "Create btree index with DESC and NULLS LAST -- 15 Plus",
"endpoint": "NODE-index.obj",
"sql_endpoint": "NODE-index.sql_id",
"msql_endpoint": "NODE-index.msql",
"data": {
"name":"Idx_$%{}[]()&*^!@\"'`\\/#",
"spcname":"pg_default",
"amname":"btree",
"columns":[{
"colname":"id",
"collspcname":"",
"op_class":"",
"sort_order":true,
"nulls":false,
"is_sort_nulls_applicable":true
}, {
"colname":"name",
"collspcname":"pg_catalog.\"POSIX\"",
"op_class":"text_pattern_ops",
"sort_order":true,
"nulls":false,
"is_sort_nulls_applicable":true
}],
"description":"Test Comment",
"fillfactor":"10",
"indisunique":true,
"indnullsnotdistinct":true,
"indisclustered":false,
"isconcurrent":false,
"indconstraint":"id < 100"
},
"expected_sql_file": "create_btree_desc_null_last.sql",
"expected_msql_file": "create_btree_desc_null_last_msql.sql"
},
{
"type": "delete",
"name": "Drop index -- 15 Plus",
"endpoint": "NODE-index.delete_id",
"data": {
"name": "Idx_$%{}[]()&*^!@\"'`\\/#"
}
},
{
"type": "create",
"name": "Create btree index with DESC and NULLS FIRST -- 15 Plus",
"endpoint": "NODE-index.obj",
"sql_endpoint": "NODE-index.sql_id",
"msql_endpoint": "NODE-index.msql",
"data": {
"name":"Idx_$%{}[]()&*^!@\"'`\\/#",
"spcname":"pg_default",
"amname":"btree",
"columns":[{
"colname":"id",
"collspcname":"",
"op_class":"",
"sort_order":true,
"nulls":true,
"is_sort_nulls_applicable":true
}, {
"colname":"name",
"collspcname":"pg_catalog.\"POSIX\"",
"op_class":"text_pattern_ops",
"sort_order":true,
"nulls":true,
"is_sort_nulls_applicable":true
}],
"indisunique":true,
"indnullsnotdistinct":true,
"indisclustered":false,
"isconcurrent":false
},
"expected_sql_file": "create_btree_desc_null_first.sql",
"expected_msql_file": "create_btree_desc_null_first_msql.sql"
},
{
"type": "delete",
"name": "Drop index -- 15 Plus",
"endpoint": "NODE-index.delete_id",
"data": {
"name": "Idx1_$%{}[]()&*^!@\"'`\\/#"
}
},
{
"type": "delete",
"name": "Drop Table",
"endpoint": "NODE-table.delete_id",
"data": {
"name": "test_table_for_indexes"
}
}
]
}

View File

@ -192,6 +192,67 @@
"error_msg": "table_id", "error_msg": "table_id",
"test_result_data": {} "test_result_data": {}
} }
},
{
"name": "Create index: Unique index",
"is_positive_test": true,
"inventory_data": {},
"test_data": {
"name": "test_index_add",
"spcname": "pg_default",
"amname": "btree",
"indisunique":true,
"indnullsnotdistinct":true,
"columns": [
{
"colname": "id",
"sort_order": false,
"nulls": false
}
],
"include": [
"name"
]
},
"mocking_required": false,
"mock_data": {},
"expected_data": {
"status_code": 200,
"error_msg": null,
"test_result_data": {}
}
},
{
"name": "Create index: Unique index With nulls not distinct.",
"is_positive_test": true,
"inventory_data": {
"server_min_version": 150000,
"skip_msg": "Nulls not distinct is not supported by PPAS/PG 15.0 and below."
},
"test_data": {
"name": "test_index_add",
"spcname": "pg_default",
"amname": "btree",
"indisunique":true,
"indnullsnotdistinct":true,
"columns": [
{
"colname": "id",
"sort_order": false,
"nulls": false
}
],
"include": [
"name"
]
},
"mocking_required": false,
"mock_data": {},
"expected_data": {
"status_code": 200,
"error_msg": null,
"test_result_data": {}
}
} }
], ],
"index_get": [ "index_get": [

View File

@ -21,6 +21,7 @@ from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils from regression.python_test_utils import test_utils as utils
from . import utils as indexes_utils from . import utils as indexes_utils
from pgadmin.utils import server_utils
class IndexesAddTestCase(BaseTestGenerator): class IndexesAddTestCase(BaseTestGenerator):
@ -31,9 +32,20 @@ class IndexesAddTestCase(BaseTestGenerator):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.db_name = parent_node_dict["database"][-1]["db_name"]
schema_info = parent_node_dict["schema"][-1] schema_info = parent_node_dict["schema"][-1]
self.server_id = schema_info["server_id"] self.server_id = schema_info["server_id"]
if "server_min_version" in self.inventory_data:
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.inventory_data["server_min_version"]:
self.skipTest(self.inventory_data["skip_msg"])
self.db_name = parent_node_dict["database"][-1]["db_name"]
self.db_id = schema_info["db_id"] self.db_id = schema_info["db_id"]
db_con = database_utils.connect_database(self, utils.SERVER_GROUP, db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.server_id, self.db_id) self.server_id, self.db_id)

View File

@ -0,0 +1,32 @@
CREATE{% if data.indisunique %} UNIQUE{% endif %} INDEX{% if add_not_exists_clause %} IF NOT EXISTS{% endif %}{% if data.isconcurrent %} CONCURRENTLY{% endif %}{% if data.name %} {{conn|qtIdent(data.name)}}{% endif %}
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 %}
{% if data.indnullsnotdistinct %}
NULLS NOT DISTINCT
{% 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 %}
{% if data.indnullsnotdistinct %}
NULLS NOT DISTINCT
{% 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,28 @@
SELECT DISTINCT ON(cls.relname) cls.oid, cls.relname as name, indrelid, indkey, indisclustered,
indisvalid, indisunique, indisprimary, n.nspname,indnatts,cls.reltablespace AS spcoid, indnullsnotdistinct,
CASE WHEN (length(spcname::text) > 0 OR cls.relkind = 'I') THEN spcname ELSE
(SELECT sp.spcname FROM pg_catalog.pg_database dtb
JOIN pg_catalog.pg_tablespace sp ON dtb.dattablespace=sp.oid
WHERE dtb.oid = {{ did }}::oid)
END as spcname,
tab.relname as tabname, indclass, con.oid AS conoid,
CASE WHEN contype IN ('p', 'u', 'x') THEN desp.description
ELSE des.description END AS description,
pg_catalog.pg_get_expr(indpred, indrelid, true) as indconstraint, contype, condeferrable, condeferred, amname,
(SELECT (CASE WHEN count(i.inhrelid) > 0 THEN true ELSE false END) FROM pg_inherits i WHERE i.inhrelid = cls.oid) as is_inherited,
substring(pg_catalog.array_to_string(cls.reloptions, ',') from 'fillfactor=([0-9]*)') AS fillfactor
{% if datlastsysoid %}, (CASE WHEN cls.oid <= {{ datlastsysoid}}::oid THEN true ElSE false END) AS is_sys_idx {% endif %}
FROM pg_catalog.pg_index idx
JOIN pg_catalog.pg_class cls ON cls.oid=indexrelid
JOIN pg_catalog.pg_class tab ON tab.oid=indrelid
LEFT OUTER JOIN pg_catalog.pg_tablespace ta on ta.oid=cls.reltablespace
JOIN pg_catalog.pg_namespace n ON n.oid=tab.relnamespace
JOIN pg_catalog.pg_am am ON am.oid=cls.relam
LEFT JOIN pg_catalog.pg_depend dep ON (dep.classid = cls.tableoid AND dep.objid = cls.oid AND dep.refobjsubid = '0' AND dep.refclassid=(SELECT oid FROM pg_catalog.pg_class WHERE relname='pg_constraint') AND dep.deptype='i')
LEFT OUTER JOIN pg_catalog.pg_constraint con ON (con.tableoid = dep.refclassid AND con.oid = dep.refobjid)
LEFT OUTER JOIN pg_catalog.pg_description des ON (des.objoid=cls.oid AND des.classoid='pg_class'::regclass)
LEFT OUTER JOIN pg_catalog.pg_description desp ON (desp.objoid=con.oid AND desp.objsubid = 0 AND desp.classoid='pg_constraint'::regclass)
WHERE indrelid = {{tid}}::OID
AND conname is NULL
{% if idx %}AND cls.oid = {{idx}}::OID {% endif %}
ORDER BY cls.relname