From 75c8987e2bca18e5d88253e1d0294dfedbce514f Mon Sep 17 00:00:00 2001 From: Julien Rouhaud Date: Sun, 9 Dec 2018 11:25:53 +0100 Subject: [PATCH] Check for (hypo) constraint compatibility with (hypo) partitioning --- expected/hypo_index_table.out | 52 +++++++++++ hypopg_index.c | 115 +++++++++++++++++++++++ hypopg_table.c | 170 ++++++++++++++++++++++++++++++++++ test/sql/hypo_index_table.sql | 36 +++++++ 4 files changed, 373 insertions(+) diff --git a/expected/hypo_index_table.out b/expected/hypo_index_table.out index 4e58cde..658e045 100644 --- a/expected/hypo_index_table.out +++ b/expected/hypo_index_table.out @@ -223,3 +223,55 @@ SELECT COUNT(*) AS nb FROM hypopg_create_index('CREATE INDEX ON hypo_part_multi_ ERROR: hypopg: cannot add hypothetical index on non-leaf or non-root hypothetical partition SELECT COUNT(*) AS nb FROM hypopg_create_index('CREATE INDEX ON hypo_part_multi_1_q1 (dpt)'); ERROR: hypopg: cannot add hypothetical index on non-leaf or non-root hypothetical partition +-- pk constraint check +CREATE TABLE t_pk(id integer primary key, val text) PARTITION BY LIST (val); +ERROR: insufficient columns in PRIMARY KEY constraint definition +DETAIL: PRIMARY KEY constraint on table "t_pk" lacks column "val" which is part of the partition key. +CREATE TABLE hypo_t_pk(id integer primary key, val text); +SELECT hypopg_partition_table('hypo_t_pk', 'PARTITION BY LIST (val)'); +ERROR: hypopg: insufficient columns in unique constraint definition +DETAIL: unique constraint on table "hypo_t_pk" lacks column "val" which is part of the hypothetical partition key. +DROP TABLE hypo_t_pk; +-- unique constraint check +CREATE TABLE t_unique(id integer, val text) PARTITION BY LIST (val); +CREATE UNIQUE INDEX ON t_unique(id); +ERROR: insufficient columns in UNIQUE constraint definition +DETAIL: UNIQUE constraint on table "t_unique" lacks column "val" which is part of the partition key. +CREATE TABLE hypo_t_unique(id integer, val text); +CREATE UNIQUE INDEX ON hypo_t_unique(id); +SELECT hypopg_partition_table('hypo_t_unique', 'PARTITION BY LIST (val)'); +ERROR: hypopg: insufficient columns in unique constraint definition +DETAIL: unique constraint on table "hypo_t_unique" lacks column "val" which is part of the hypothetical partition key. +DROP TABLE hypo_t_unique; +CREATE TABLE hypo_t_unique(id integer, val text); +SELECT count(*) FROM hypopg_create_index('CREATE UNIQUE INDEX on hypo_t_unique (id)'); + count +------- + 1 +(1 row) + +SELECT hypopg_partition_table('hypo_t_unique', 'PARTITION BY LIST (val)'); +ERROR: hypopg: insufficient columns in unique hypothetical constraint definition +DETAIL: unique constraint on table "hypo_t_unique" lacks column "val" which is part of the hypothetical partition key. +DROP TABLE hypo_t_unique; +CREATE TABLE hypo_t_unique(id integer, val text); +SELECT hypopg_partition_table('hypo_t_unique', 'PARTITION BY LIST (val)'); + hypopg_partition_table +------------------------ + t +(1 row) + +SELECT count(*) FROM hypopg_create_index('CREATE UNIQUE INDEX on hypo_t_unique (id)'); +ERROR: hypopg: insufficient columns in unique hypothetical constraint definition +DETAIL: unique hypothetical constraint on table "hypo_t_unique" lacks column "val" which is part of the hypothetical partition key. +DROP TABLE t_unique; +DROP TABLE hypo_t_unique; +-- constraint exclusion check +CREATE TABLE t_constrext(c circle, val text, EXCLUDE USING gist(c WITH &&)) PARTITION BY LIST (val); +ERROR: exclusion constraints are not supported on partitioned tables +LINE 1: CREATE TABLE t_constrext(c circle, val text, EXCLUDE USING g... + ^ +CREATE TABLE hypo_t_constrext(c circle, val text, EXCLUDE USING gist(c WITH &&)); +SELECT hypopg_partition_table('hypo_t_constrext', 'PARTITION BY LIST (val)'); +ERROR: exclusion constraints are not supported on hypothetically partitioned tables +DROP TABLE hypo_t_constrext; diff --git a/hypopg_index.c b/hypopg_index.c index 4688352..40348e5 100644 --- a/hypopg_index.c +++ b/hypopg_index.c @@ -57,6 +57,9 @@ #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#if PG_VERSION_NUM >= 110000 +#include "utils/partcache.h" +#endif #if PG_VERSION_NUM >= 90500 #include "utils/ruleutils.h" #endif @@ -98,6 +101,8 @@ static void hypo_estimate_index_simple(hypoIndex *entry, static void hypo_estimate_index(hypoIndex *entry, RelOptInfo *rel, PlannerInfo *root); static int hypo_estimate_index_colsize(hypoIndex *entry, int col); +static void hypo_index_check_uniqueness_compatibility(IndexStmt *stmt, + Oid relid, hypoIndex *entry); static void hypo_index_pfree(hypoIndex *entry); static bool hypo_index_remove(Oid indexid); static const hypoIndex *hypo_index_store_parsetree(IndexStmt *node, @@ -399,6 +404,7 @@ hypo_index_store_parsetree(IndexStmt *node, const char *queryString) node = transformIndexStmt(relid, node, queryString); nkeycolumns = list_length(node->indexParams); + #if PG_VERSION_NUM >= 110000 if (list_intersection(node->indexParams, node->indexIncludingParams) != NIL) ereport(ERROR, @@ -689,6 +695,14 @@ hypo_index_store_parsetree(IndexStmt *node, const char *queryString) errmsg("hypopg: index creation on system columns is not supported"))); } +#if PG_VERSION_NUM >= 110000 + /* + * check for uniqueness compatibility with (hypothetically) partitioned + * tables + */ + hypo_index_check_uniqueness_compatibility(node, relid, entry); +#endif + #if PG_VERSION_NUM >= 110000 attn = nkeycolumns; @@ -907,6 +921,107 @@ hypo_index_remove(Oid indexid) return false; } +#if PG_VERSION_NUM >= 110000 +/* + * If this table is partitioned and we're creating a unique index or a + * primary key, make sure that the indexed columns are part of the + * partition key. Otherwise it would be possible to violate uniqueness by + * putting values that ought to be unique in different partitions. + * + * We could lift this limitation if we had global indexes, but those have + * their own problems, so this is a useful feature combination. + * + * Heavily inspired on DefineIndex. + */ +static void +hypo_index_check_uniqueness_compatibility(IndexStmt *stmt, Oid relid, + hypoIndex *entry) +{ + PartitionKey key = NULL; + Relation rel; + const char *extra; + int i; + + if (!stmt->unique) + return; + + rel = relation_open(relid, AccessShareLock); + + if (hypo_table_oid_is_hypothetical(relid)) + { + hypoTable *table = hypo_find_table(relid, false); + + key = table->partkey; + extra = "hypothetical "; + } + else + { + key = rel->rd_partkey; + extra = ""; + } + + if (!key) + { + relation_close(rel, AccessShareLock); + return; + } + + /* + * A partitioned table can have unique indexes, as long as all the + * columns in the partition key appear in the unique key. A + * partition-local index can enforce global uniqueness iff the PK + * value completely determines the partition that a row is in. + * + * Thus, verify that all the columns in the partition key appear in + * the unique key definition. + */ + for (i = 0; i < key->partnatts; i++) + { + bool found = false; + int j; + const char *constraint_type = "unique"; + + /* + * It may be possible to support UNIQUE constraints when partition + * keys are expressions, but is it worth it? Give up for now. + */ + if (key->partattrs[i] == 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypopg: unsupported %s hypothetical constraint with %spartition key definition", + constraint_type, extra), + errdetail("%s hypothetical constraints cannot be used when %spartition keys include expressions.", + constraint_type, extra))); + + for (j = 0; j < entry->nkeycolumns; j++) + { + if (key->partattrs[i] == entry->indexkeys[j]) + { + found = true; + break; + } + } + if (!found) + { + Form_pg_attribute att; + + att = TupleDescAttr(RelationGetDescr(rel), key->partattrs[i] - 1); + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypopg: insufficient columns in %s hypothetical constraint definition", + constraint_type), + errdetail("%s hypothetical constraint on table \"%s\" lacks column \"%s\" which is part of the %spartition key.", + constraint_type, RelationGetRelationName(rel), + NameStr(att->attname), + extra))); + } + } + + relation_close(rel, AccessShareLock); + +} +#endif + /* pfree all allocated memory for within an hypoIndex and the entry itself. */ static void hypo_index_pfree(hypoIndex *entry) diff --git a/hypopg_table.c b/hypopg_table.c index 5a31aed..0132ed8 100644 --- a/hypopg_table.c +++ b/hypopg_table.c @@ -25,6 +25,7 @@ #include "access/hash.h" #include "access/htup_details.h" #include "access/nbtree.h" +#include "catalog/index.h" #include "catalog/namespace.h" #include "catalog/partition.h" #include "catalog/pg_class.h" @@ -63,6 +64,7 @@ #include "include/hypopg.h" #include "include/hypopg_analyze.h" +#include "include/hypopg_index.h" #include "include/hypopg_table.h" /*--- Variables exported ---*/ @@ -113,6 +115,9 @@ static Oid hypo_get_default_partition_oid(hypoTable *parent); static char *hypo_get_partbounddef(hypoTable *entry); static char *hypo_get_partkeydef(hypoTable *entry); static hypoTable *hypo_newTable(Oid parentid); +#if PG_VERSION_NUM >= 110000 +static void hypo_table_check_constraints_compatibility(hypoTable *table); +#endif static hypoTable *hypo_table_find_parent_oid(Oid parentid); static void hypo_table_pfree(hypoTable *entry, bool freeFieldsOnly); static hypoTable *hypo_table_store_parsetree(CreateStmt *node, @@ -1654,6 +1659,162 @@ hypo_get_partkeydef(hypoTable *entry) return buf.data; } +#if PG_VERSION_NUM >= 110000 +/* + * check if the wanted PARTITION BY clause is compatible with any exiting + * unique/pk index. + * + * Heavily inspired on get_relation_info and DefineIndex + */ +static void +hypo_table_check_constraints_compatibility(hypoTable *table) +{ + Relation rel = heap_open(table->rootid, AccessShareLock); + List *idxlist = RelationGetIndexList(rel); + PartitionKey key = table->partkey; + ListCell *lc; + + /* The partey should have already been generated */ + Assert(key); + + /* adapted from DefineIndex */ + foreach(lc, idxlist) + { + Relation idxRel = index_open(lfirst_oid(lc), AccessShareLock); + IndexInfo *indexInfo = BuildIndexInfo(idxRel); + Form_pg_index index; + int i; + + index = idxRel->rd_index; + + if (!IndexIsValid(index)) + { + index_close(idxRel, AccessShareLock); + continue; + } + + if (index->indisexclusion) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("exclusion constraints are not supported on hypothetically partitioned tables"))); + } + + if (!index->indisunique) + { + index_close(idxRel, AccessShareLock); + continue; + } + + /* + * A partitioned table can have unique indexes, as long as all the + * columns in the partition key appear in the unique key. A + * partition-local index can enforce global uniqueness iff the PK + * value completely determines the partition that a row is in. + * + * Thus, verify that all the columns in the partition key appear in + * the unique key definition. + */ + for (i = 0; i < key->partnatts; i++) + { + bool found = false; + int j; + /* FIXME detect the real constraint type */ + const char *constraint_type = "unique"; + + /* + * It may be possible to support UNIQUE constraints when partition + * keys are expressions, but is it worth it? Give up for now. + */ + if (key->partattrs[i] == 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypopg: unsupported %s constraint with hypothetical partition key definition", + constraint_type), + errdetail("%s constraints cannot be used when hypothetical partition keys include expressions.", + constraint_type))); + + for (j = 0; j < indexInfo->ii_NumIndexAttrs; j++) + { + if (key->partattrs[i] == indexInfo->ii_IndexAttrNumbers[j]) + { + found = true; + break; + } + } + if (!found) + { + Form_pg_attribute att; + + att = TupleDescAttr(RelationGetDescr(rel), key->partattrs[i] - 1); + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypopg: insufficient columns in %s constraint definition", + constraint_type), + errdetail("%s constraint on table \"%s\" lacks column \"%s\" which is part of the hypothetical partition key.", + constraint_type, RelationGetRelationName(rel), + NameStr(att->attname)))); + } + + } + + index_close(idxRel, AccessShareLock); + } + + /* same, but for hypothetical indexes */ + foreach(lc, hypoIndexes) + { + hypoIndex *idx = (hypoIndex *) lfirst(lc); + int i; + + if ((idx->relid != table->rootid && idx->relid != table->oid) || + !idx->unique) + continue; + + for (i = 0; i < key->partnatts; i++) + { + bool found = false; + int j; + /* FIXME detect the real constraint type */ + const char *constraint_type = "unique"; + + if (key->partattrs[i] == 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypopg: unsupported hypothetical %s constraint with hypothetical partition key definition", + constraint_type), + errdetail("hypothetical %s constraints cannot be used when hypothetical partition keys include expressions.", + constraint_type))); + + for (j = 0; j < idx->nkeycolumns; j++) + { + if (key->partattrs[i] == idx->indexkeys[j]) + { + found = true; + break; + } + } + if (!found) + { + Form_pg_attribute att; + + att = TupleDescAttr(RelationGetDescr(rel), key->partattrs[i] - 1); + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypopg: insufficient columns in %s hypothetical constraint definition", + constraint_type), + errdetail("%s constraint on table \"%s\" lacks column \"%s\" which is part of the hypothetical partition key.", + constraint_type, RelationGetRelationName(rel), + NameStr(att->attname)))); + } + + } + } + + heap_close(rel, AccessShareLock); +} +#endif + /* * palloc a new hypoTable, and give it a new OID, and some other global stuff. */ @@ -1959,7 +2120,16 @@ hypo_table_store_parsetree(CreateStmt *node, const char *queryString, /* The CreateStmt specified a PARTITION BY clause, store it */ if (stmt->partspec) + { hypo_generate_partkey(stmt, rootid, entry); +#if PG_VERSION_NUM >= 110000 + /* + * Make sure that the partitioning clause is compatible with + * existing constraints + */ + hypo_table_check_constraints_compatibility(entry); +#endif + } if (boundspec) { diff --git a/test/sql/hypo_index_table.sql b/test/sql/hypo_index_table.sql index 16d9785..6025bd4 100644 --- a/test/sql/hypo_index_table.sql +++ b/test/sql/hypo_index_table.sql @@ -100,3 +100,39 @@ SELECT COUNT(*) AS nb FROM hypopg_create_index('CREATE INDEX ON part_multi_1_q1 SELECT COUNT(*) AS nb FROM hypopg_create_index('CREATE INDEX ON hypo_part_multi_1 (dpt)'); SELECT COUNT(*) AS nb FROM hypopg_create_index('CREATE INDEX ON hypo_part_multi_1_q1 (dpt)'); + +-- pk constraint check +CREATE TABLE t_pk(id integer primary key, val text) PARTITION BY LIST (val); + +CREATE TABLE hypo_t_pk(id integer primary key, val text); +SELECT hypopg_partition_table('hypo_t_pk', 'PARTITION BY LIST (val)'); + +DROP TABLE hypo_t_pk; +-- unique constraint check +CREATE TABLE t_unique(id integer, val text) PARTITION BY LIST (val); +CREATE UNIQUE INDEX ON t_unique(id); + +CREATE TABLE hypo_t_unique(id integer, val text); +CREATE UNIQUE INDEX ON hypo_t_unique(id); +SELECT hypopg_partition_table('hypo_t_unique', 'PARTITION BY LIST (val)'); + +DROP TABLE hypo_t_unique; +CREATE TABLE hypo_t_unique(id integer, val text); +SELECT count(*) FROM hypopg_create_index('CREATE UNIQUE INDEX on hypo_t_unique (id)'); +SELECT hypopg_partition_table('hypo_t_unique', 'PARTITION BY LIST (val)'); + +DROP TABLE hypo_t_unique; +CREATE TABLE hypo_t_unique(id integer, val text); +SELECT hypopg_partition_table('hypo_t_unique', 'PARTITION BY LIST (val)'); +SELECT count(*) FROM hypopg_create_index('CREATE UNIQUE INDEX on hypo_t_unique (id)'); + +DROP TABLE t_unique; +DROP TABLE hypo_t_unique; + +-- constraint exclusion check +CREATE TABLE t_constrext(c circle, val text, EXCLUDE USING gist(c WITH &&)) PARTITION BY LIST (val); + +CREATE TABLE hypo_t_constrext(c circle, val text, EXCLUDE USING gist(c WITH &&)); +SELECT hypopg_partition_table('hypo_t_constrext', 'PARTITION BY LIST (val)'); + +DROP TABLE hypo_t_constrext;