>From 5c14a07462bd71a6dd4c9777ce15df96204105ab Mon Sep 17 00:00:00 2001 From: amit Date: Thu, 14 Jul 2016 14:38:08 +0900 Subject: [PATCH 3/7] Catalog and DDL for partitions. pg_class gets new fields: relispartition and relpartbound (a pg_node_tree). Parent-child relationships of a partitioned table and its partitions are managed with inheritance, so not much here about that. DDL includes both a way to create new partition and "attach" an existing table as partition. An existing partition can be "detached" from a table, which preserves it as a regular (or partitioned) table. Add a field to relcache for storing a "partition descriptor" of a partitioned table which has hopefully all the information about a table's partitions that someone might want to do something with. --- doc/src/sgml/catalogs.sgml | 17 + doc/src/sgml/ref/alter_table.sgml | 105 ++- doc/src/sgml/ref/create_foreign_table.sgml | 26 + doc/src/sgml/ref/create_table.sgml | 92 ++- src/backend/catalog/Makefile | 2 +- src/backend/catalog/heap.c | 105 ++- src/backend/catalog/partition.c | 1550 ++++++++++++++++++++++++++++ src/backend/commands/createas.c | 2 +- src/backend/commands/sequence.c | 2 +- src/backend/commands/tablecmds.c | 1008 ++++++++++++++++-- src/backend/commands/typecmds.c | 3 +- src/backend/commands/view.c | 3 +- src/backend/nodes/copyfuncs.c | 47 + src/backend/nodes/equalfuncs.c | 41 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/nodes/outfuncs.c | 27 + src/backend/nodes/readfuncs.c | 34 + src/backend/parser/gram.y | 208 ++++- src/backend/parser/parse_utilcmd.c | 260 +++++- src/backend/tcop/utility.c | 6 +- src/backend/utils/cache/relcache.c | 93 ++- src/include/catalog/heap.h | 1 + src/include/catalog/partition.h | 48 + src/include/catalog/pg_class.h | 22 +- src/include/commands/tablecmds.h | 2 +- src/include/nodes/nodes.h | 3 + src/include/nodes/parsenodes.h | 52 +- src/include/parser/kwlist.h | 2 + src/include/parser/parse_utilcmd.h | 2 + src/include/utils/rel.h | 21 + src/test/regress/expected/alter_table.out | 291 ++++++ src/test/regress/expected/create_table.out | 187 ++++ src/test/regress/sql/alter_table.sql | 256 +++++ src/test/regress/sql/create_table.sql | 153 +++ 34 files changed, 4538 insertions(+), 139 deletions(-) create mode 100644 src/backend/catalog/partition.c create mode 100644 src/include/catalog/partition.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 26e5ce8..7b7b5e8 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1846,6 +1846,13 @@ + relispartition + bool + + True if table is a partition + + + relfrozenxid xid @@ -1891,6 +1898,16 @@ Access-method-specific options, as keyword=value strings + + + relpartbound + pg_node_tree + + + If table is a partition (see relispartition), + internal representation of the partition bound + + diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index e48ccf2..5949837 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -77,6 +77,8 @@ ALTER TABLE ALL IN TABLESPACE name NOT OF OWNER TO { new_owner | CURRENT_USER | SESSION_USER } REPLICA IDENTITY { DEFAULT | USING INDEX index_name | FULL | NOTHING } + ATTACH PARTITION partition_name partition_bound_spec + DETACH PARTITION partition_name and table_constraint_using_index is: @@ -166,6 +168,12 @@ ALTER TABLE ALL IN TABLESPACE name values or to reject null values. You can only use SET NOT NULL when the column contains no null values. + + + If this table is a partition, one cannot perform DROP NOT NULL + on a column if it is marked NOT NULL in the parent + table. + @@ -704,6 +712,52 @@ ALTER TABLE ALL IN TABLESPACE name + + ATTACH PARTITION + + + This form attaches an existing table (which might itself be partitioned) + as a partition of the target table. The partition bound specification + must correspond to the partitioning strategy and partition key of the + target table. The table to be attached must have all the same columns + as the target table and no more; moreover, the column types must also + match. Also, it must have all the NOT NULL and + CHECK constraints present in the target table. + If some CHECK constraint of the table being attached + is marked NO INHERIT, the command will fail; such + constraints must be recreated without the NO INHERIT + clause. Currently UNIQUE, PRIMARY KEY, + and FOREIGN KEY constraints are not considered. + + + + A full table scan is performed on the table being attached to check that + no existing row in the table violates the partition constraint. It is + possible to avoid this potentially expensive scan by adding a valid + CHECK constraint to the table that only allows rows + satisfying the desired partition constraint before trying to attach. + It will be determined using such a constraint that existing rows in the + table satisfy the partition constraint, so the table scan to check the + same will be skipped. When adding a range partition or a list partition + that does not accept NULL values, also add + NOT NULL constraint to the partition key columns, + otherwise, the scan will be performed regardless of the existing + constraints. + + + + + + DETACH PARTITION + + + This form detaches specified partition of the target table. The detached + partition continues to exist as a standalone table, but no longer has any + ties to the table from which it was detached. + + + + @@ -721,8 +775,8 @@ ALTER TABLE ALL IN TABLESPACE name You must own the table to use ALTER TABLE. To change the schema or tablespace of a table, you must also have CREATE privilege on the new schema or tablespace. - To add the table as a new child of a parent table, you must own the - parent table as well. + To add the table as a new child of a parent table, or as a new partition + of a partitioned table, you must own the parent table as well. To alter the owner, you must also be a direct or indirect member of the new owning role, and that role must have CREATE privilege on the table's schema. (These restrictions enforce that altering the owner @@ -938,6 +992,24 @@ ALTER TABLE ALL IN TABLESPACE name + + partition_name + + + The name of the table to attach as a new partition or to detach from this table. + + + + + + partition_bound_spec + + + The partition bound specification for a new partition. + + + + @@ -978,6 +1050,11 @@ ALTER TABLE ALL IN TABLESPACE name + Similarly, when attaching a new partition it is scanned to verify that + existing rows meet the partition constraint. + + + The main reason for providing the option to specify multiple changes in a single ALTER TABLE is that multiple table scans or rewrites can thereby be combined into a single pass over the table. @@ -1047,6 +1124,9 @@ ALTER TABLE ALL IN TABLESPACE name COLUMN (i.e., ALTER TABLE ONLY ... DROP COLUMN) never removes any descendant columns, but instead marks them as independently defined rather than inherited. + A nonrecursive DROP COLUMN command will fail for a + partitioned table, because all partitions of a table must have the same + columns as the partitioning root. @@ -1233,6 +1313,27 @@ ALTER TABLE distributors DROP CONSTRAINT distributors_pkey, ADD CONSTRAINT distributors_pkey PRIMARY KEY USING INDEX dist_id_temp_idx; + + Attach a partition to range partitioned table: + +ALTER TABLE measurement + ATTACH PARTITION measurement_y2016m07 FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); + + + + Attach a partition to list partitioned table: + +ALTER TABLE cities + ATTACH PARTITION cities_west FOR VALUES IN ('Los Angeles', 'San Francisco'); + + + + Detach a partition from partitioned table: + +ALTER TABLE cities + DETACH PARTITION measurement_y2015m12; + + diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml index 413b033..5d0dcf5 100644 --- a/doc/src/sgml/ref/create_foreign_table.sgml +++ b/doc/src/sgml/ref/create_foreign_table.sgml @@ -27,6 +27,15 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] table_name SERVER server_name [ OPTIONS ( option 'value' [, ... ] ) ] +CREATE FOREIGN TABLE [ IF NOT EXISTS ] table_name + PARTITION OF parent_table [ ( + { column_name WITH OPTIONS [ column_constraint [ ... ] ] + | table_constraint } + [, ... ] +) ] partition_bound_spec + SERVER server_name +[ OPTIONS ( option 'value' [, ... ] ) ] + where column_constraint is: [ CONSTRAINT constraint_name ] @@ -68,6 +77,12 @@ CHECK ( expression ) [ NO INHERIT ] + If PARTITION OF clause is specified then the table is + created as a partition of parent_table with specified + bounds. + + + To be able to create a foreign table, you must have USAGE privilege on the foreign server, as well as USAGE privilege on all column types used in the table. @@ -314,6 +329,17 @@ CREATE FOREIGN TABLE films ( SERVER film_server; + + Create foreign table measurement_y2016m07, which will be + accessed through the server server_07, as a partition + of the range partitioned table measurement: + + +CREATE FOREIGN TABLE measurement_y2016m07 + PARTITION OF measurement FOR VALUES FROM ('2016-07-01') TO ('2016-08-01') + SERVER server_07; + + diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index eeb9d59..27a391e 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -44,6 +44,17 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI [ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ] [ TABLESPACE tablespace_name ] +CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name + PARTITION OF parent_table [ ( + { column_name WITH OPTIONS [ column_constraint [ ... ] ] + | table_constraint } + [, ... ] +) ] partition_bound_spec +[ PARTITION BY { RANGE | LIST } ( { column_name | ( expression ) } [ opclass ] [, ...] ) +[ WITH ( storage_parameter [= value] [, ... ] ) | WITH OIDS | WITHOUT OIDS ] +[ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ] +[ TABLESPACE tablespace_name ] + where column_constraint is: [ CONSTRAINT constraint_name ] @@ -72,6 +83,10 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI { INCLUDING | EXCLUDING } { DEFAULTS | CONSTRAINTS | INDEXES | STORAGE | COMMENTS | ALL } +and partition_bound_spec is: + +FOR VALUES { IN ( expression [, ...] ) | FROM ( { expression | UNBOUNDED } [, ...] ) TO ( { expression | UNBOUNDED } [, ...] ) } + index_parameters in UNIQUE, PRIMARY KEY, and EXCLUDE constraints are: [ WITH ( storage_parameter [= value] [, ... ] ) ] @@ -232,6 +247,50 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI + PARTITION OF parent_table + + + Creates the table as partition of the specified + parent table. + + + + The partition bound specification must correspond to the partitioning + method and partition key of the parent table, and must not overlap with + any existing partition of that parent. + + + + A partition cannot have columns other than those inherited from the + parent. That includes the oid column, which can be + specified using the WITH (OIDS) clause. + Defaults and constraints can optionally be specified for each of the + inherited columns. One can also specify table constraints in addition + to those inherited from the parent. If a check constraint with the name + matching one of the parent's constraint is specified, it is merged with + the latter, provided the specified condition is same. + + + + Rows inserted into a partitioned table will be automatically routed to + the correct partition. If no suitable partition exists, an error will + occur. + + + + A partition must have the same column names and types as the table of + which it is a partition. Therefore, modifications to the column names + or types of the partitioned table will automatically propagate to all + children, as will operations such as TRUNCATE which normally affect a + table and all of its inheritance children. It is also possible to + TRUNCATE a partition individually, just as for an inheritance child. + Dropping a partition directly using DROP TABLE + will fail; it must be detached from the parent first. + + + + + column_name @@ -1429,7 +1488,38 @@ CREATE TABLE measurement ( CREATE TABLE cities ( name text not null, population int, -) PARTITION BY LIST (name); +) PARTITION BY LIST (initcap(name)); + + + + Create partition of a range partitioned table: + +CREATE TABLE measurement_y2016m07 + PARTITION OF measurement ( + unitsales WITH OPTIONS DEFAULT 0 +) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); + + + + Create partition of a list partitioned table: + +CREATE TABLE cities_west + PARTITION OF cities ( + CONSTRAINT city_id_nonzero CHECK (city_id != 0) +) FOR VALUES IN ('Los Angeles', 'San Francisco'); + + + + Create partition of a list partitioned table that is itself further + partitioned and then add a partition to it: + +CREATE TABLE cities_west + PARTITION OF cities ( + CONSTRAINT city_id_nonzero CHECK (city_id != 0) +) FOR VALUES IN ('Los Angeles', 'San Francisco') PARTITION BY RANGE (population); + +CREATE TABLE cities_west_10000_to_100000 + PARTITION OF cities_west FOR VALUES FROM (10000) TO (100000); diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index 362deca..2d5ac09 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -11,7 +11,7 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \ - objectaccess.o objectaddress.o pg_aggregate.o pg_collation.o \ + objectaccess.o objectaddress.o partition.o pg_aggregate.o pg_collation.o \ pg_constraint.o pg_conversion.o \ pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \ pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \ diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index d7ce4ca..9729bfa 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -41,6 +41,7 @@ #include "catalog/heap.h" #include "catalog/index.h" #include "catalog/objectaccess.h" +#include "catalog/partition.h" #include "catalog/pg_attrdef.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" @@ -810,6 +811,7 @@ InsertPgClassTuple(Relation pg_class_desc, values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass); values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated); values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident); + values[Anum_pg_class_relispartition - 1] = BoolGetDatum(rd_rel->relispartition); values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid); values[Anum_pg_class_relminmxid - 1] = MultiXactIdGetDatum(rd_rel->relminmxid); if (relacl != (Datum) 0) @@ -821,6 +823,9 @@ InsertPgClassTuple(Relation pg_class_desc, else nulls[Anum_pg_class_reloptions - 1] = true; + /* relpartbound is set by updating this tuple, if necessary */ + nulls[Anum_pg_class_relpartbound - 1] = true; + tup = heap_form_tuple(RelationGetDescr(pg_class_desc), values, nulls); /* @@ -926,6 +931,9 @@ AddNewRelationTuple(Relation pg_class_desc, new_rel_reltup->reltype = new_type_oid; new_rel_reltup->reloftype = reloftype; + /* relispartition is always set by updating this tuple later */ + new_rel_reltup->relispartition = false; + new_rel_desc->rd_att->tdtypeid = new_type_oid; /* Now build and insert the tuple */ @@ -1766,6 +1774,8 @@ void heap_drop_with_catalog(Oid relid) { Relation rel; + Oid parentOid; + Relation parent = NULL; /* * Open and lock the relation. @@ -1773,6 +1783,21 @@ heap_drop_with_catalog(Oid relid) rel = relation_open(relid, AccessExclusiveLock); /* + * If the relation is a partition, we must grab exclusive lock on its + * parent because we need to update its partition descriptor. We must + * take a table lock strong enough to prevent all queries on the parent + * from proceeding until we commit and send out a shared-cache-inval + * notice that will make them update their partition descriptor. + * Sometimes, doing this is cycles spent uselessly, especially if the + * parent will be dropped as part of the same command anyway. + */ + if (rel->rd_rel->relispartition) + { + parentOid = get_partition_parent(relid); + parent = heap_open(parentOid, AccessExclusiveLock); + } + + /* * There can no longer be anyone *else* touching the relation, but we * might still have open queries or cursors, or pending trigger events, in * our own session. @@ -1863,6 +1888,12 @@ heap_drop_with_catalog(Oid relid) * delete relation tuple */ DeleteRelationTuple(relid); + + if (parent) + { + CacheInvalidateRelcache(parent); + heap_close(parent, NoLock); /* keep the lock */ + } } @@ -2469,8 +2500,11 @@ MergeWithExistingConstraint(Relation rel, char *ccname, Node *expr, * definition) then interpret addition of a local constraint as a * legal merge. This allows ALTER ADD CONSTRAINT on parent and * child tables to be given in either order with same end state. + * However if the relation is a partition, all inherited + * constraints are always non-local, including those that were + * merged. */ - if (is_local && !con->conislocal) + if (is_local && !con->conislocal && !rel->rd_rel->relispartition) allow_merge = true; if (!found || !allow_merge) @@ -2515,10 +2549,24 @@ MergeWithExistingConstraint(Relation rel, char *ccname, Node *expr, tup = heap_copytuple(tup); con = (Form_pg_constraint) GETSTRUCT(tup); - if (is_local) - con->conislocal = true; + /* + * In case of partitions, an inherited constraint must be + * inherited only once since it cannot have multiple parents and + * it is never considered local. + */ + if (rel->rd_rel->relispartition) + { + con->coninhcount = 1; + con->conislocal = false; + } else - con->coninhcount++; + { + if (is_local) + con->conislocal = true; + else + con->coninhcount++; + } + if (is_no_inherit) { Assert(is_local); @@ -3177,3 +3225,52 @@ RemovePartitionKeyByRelId(Oid relid) ReleaseSysCache(tuple); heap_close(rel, RowExclusiveLock); } + +/* + * StorePartitionBound + * Update pg_class tuple of rel to store the partition bound and set + * relispartition to true + */ +void +StorePartitionBound(Relation rel, Node *bound) +{ + Relation classRel; + HeapTuple tuple, + newtuple; + Datum new_val[Natts_pg_class]; + bool new_null[Natts_pg_class], + new_repl[Natts_pg_class]; + + /* Update pg_class tuple */ + classRel = heap_open(RelationRelationId, RowExclusiveLock); + tuple = SearchSysCacheCopy1(RELOID, + ObjectIdGetDatum(RelationGetRelid(rel))); +#ifdef USE_ASSERT_CHECKING + { + Form_pg_class classForm; + bool isnull; + + classForm = (Form_pg_class) GETSTRUCT(tuple); + Assert(!classForm->relispartition); + (void) SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relpartbound, + &isnull); + Assert(isnull); + } +#endif + + /* Fill in relpartbound value */ + memset(new_val, 0, sizeof(new_val)); + memset(new_null, false, sizeof(new_null)); + memset(new_repl, false, sizeof(new_repl)); + new_val[Anum_pg_class_relpartbound - 1] = CStringGetTextDatum(nodeToString(bound)); + new_null[Anum_pg_class_relpartbound - 1] = false; + new_repl[Anum_pg_class_relpartbound - 1] = true; + newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel), + new_val, new_null, new_repl); + /* Also set the flag */ + ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = true; + simple_heap_update(classRel, &newtuple->t_self, newtuple); + CatalogUpdateIndexes(classRel, newtuple); + heap_freetuple(newtuple); + heap_close(classRel, RowExclusiveLock); +} diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c new file mode 100644 index 0000000..197fcef --- /dev/null +++ b/src/backend/catalog/partition.c @@ -0,0 +1,1550 @@ +/*------------------------------------------------------------------------- + * + * partition.c + * Partitioning related data structures and functions. + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/catalog/partition.c + * + *------------------------------------------------------------------------- +*/ + +#include "postgres.h" + +#include "access/heapam.h" +#include "access/htup_details.h" +#include "access/nbtree.h" +#include "access/sysattr.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/objectaddress.h" +#include "catalog/partition.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_inherits.h" +#include "catalog/pg_inherits_fn.h" +#include "catalog/pg_opclass.h" +#include "catalog/pg_type.h" +#include "executor/executor.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "nodes/parsenodes.h" +#include "optimizer/clauses.h" +#include "optimizer/planmain.h" +#include "optimizer/var.h" +#include "rewrite/rewriteManip.h" +#include "storage/lmgr.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/memutils.h" +#include "utils/fmgroids.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/ruleutils.h" +#include "utils/syscache.h" + +/* + * Information about bounds of a partitioned relation + * + * A list partition datum that is known to be NULL is never put into the + * datums array, instead it is tracked using has_null and null_index fields. + * + * In case of range partitioning, ndatums is far less than 2 * nparts, because + * a partition's upper bound and the next partition's lower bound are same + * in most common cases, and we only store one of them. + * + * In case of list partitioning, the indexes array stores one entry for every + * datum, which is the index of the partition that accepts a given datum. + * Wheareas, in case of range partitioning, it stores one entry per distinct + * range datum, which is the index of the partition of which a given datum + * is an upper bound. + */ + +/* Ternary value to represent what's contained in a range bound datum */ +typedef enum RangeDatumContent +{ + RANGE_DATUM_FINITE = 0, /* actual datum stored elsewhere */ + RANGE_DATUM_NEG_INF, /* negative infinity */ + RANGE_DATUM_POS_INF /* positive infinity */ +} RangeDatumContent; + +typedef struct PartitionBoundInfoData +{ + char strategy; /* list or range bounds? */ + int ndatums; /* Length of the datums following array */ + Datum **datums; /* Array of datum-tuples with key->partnatts + * datums each */ + RangeDatumContent **content; /* what's contained in each range bound + * datum? (see the above enum); NULL for + * list partitioned tables */ + int *indexes; /* Partition indexes; one entry per member of + * the datums array (plus one if range + * partitioned table) */ + bool has_null; /* Is there a null-accepting partition? false + * for range partitioned tables */ + int null_index; /* Index of the null-accepting partition; -1 + * for range partitioned tables */ +} PartitionBoundInfoData; + +/* + * When qsort'ing partition bounds after reading from the catalog, each bound + * is represented with one of the following structs. + */ + +/* One value coming from some (index'th) list partition */ +typedef struct PartitionListValue +{ + int index; + Datum value; +} PartitionListValue; + +/* One bound of a range partition */ +typedef struct PartitionRangeBound +{ + int index; + Datum *datums; /* range bound datums */ + RangeDatumContent *content; /* what's contained in each datum?*/ + bool lower; /* this is the lower (vs upper) bound */ +} PartitionRangeBound; + +static int32 qsort_partition_list_value_cmp(const void *a, const void *b, void *arg); +static int32 qsort_partition_rbound_cmp(const void *a, const void *b, void *arg); + +static List *get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec); +static List *get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec); +static Oid get_partition_operator(PartitionKey key, int col, StrategyNumber strategy, + bool *need_relabel); +static List *generate_partition_qual(Relation rel, bool recurse); + +static PartitionRangeBound *make_one_range_bound(PartitionKey key, int index, List *datums, bool lower); +static int32 partition_rbound_cmp(PartitionKey key, + Datum *datums1, RangeDatumContent *content1, bool lower1, + PartitionRangeBound *b2); + +static int32 partition_bound_cmp(PartitionKey key, PartitionBoundInfo boundinfo, + int offset, void *probe, bool probe_is_bound); +static int partition_bound_bsearch(PartitionKey key, PartitionBoundInfo boundinfo, + void *probe, bool probe_is_bound, bool *is_equal); + +/* + * RelationBuildPartitionDesc + * Form rel's partition descriptor + * + * Not flushed from the cache by RelationClearRelation() unless changed because + * of addition or removal of partition. + */ +void +RelationBuildPartitionDesc(Relation rel) +{ + List *inhoids, + *partoids; + Oid *oids = NULL; + List *boundspecs = NIL; + ListCell *cell; + int i, + nparts; + PartitionKey key = RelationGetPartitionKey(rel); + PartitionDesc result; + MemoryContext oldcxt; + + int ndatums; + + /* List partitioning specific */ + PartitionListValue **all_values = NULL; + bool found_null = false; + int null_index = -1; + + /* Range partitioning specific */ + PartitionRangeBound **rbounds = NULL; + + /* + * The following could happen in situations where rel has a pg_class + * entry but not the pg_partitioned_table entry yet. + */ + if (key == NULL) + return; + + /* Get partition oids from pg_inherits */ + inhoids = find_inheritance_children(RelationGetRelid(rel), NoLock); + + /* Collect bound spec nodes in a list */ + i = 0; + partoids = NIL; + foreach(cell, inhoids) + { + Oid inhrelid = lfirst_oid(cell); + HeapTuple tuple; + Datum datum; + bool isnull; + Node *boundspec; + + tuple = SearchSysCache1(RELOID, inhrelid); + + /* + * It is possible that the pg_class tuple of a partition has not been + * updated yet to set its relpartbound field. The only case where + * this happens is when we open the parent relation to check using its + * partition descriptor that a new partition's bound does not overlap + * some existing partition. + */ + if (!((Form_pg_class) GETSTRUCT(tuple))->relispartition) + { + ReleaseSysCache(tuple); + continue; + } + + datum = SysCacheGetAttr(RELOID, tuple, + Anum_pg_class_relpartbound, + &isnull); + Assert(!isnull); + boundspec = (Node *) stringToNode(TextDatumGetCString(datum)); + boundspecs = lappend(boundspecs, boundspec); + partoids = lappend_oid(partoids, inhrelid); + ReleaseSysCache(tuple); + } + + nparts = list_length(partoids); + + if (nparts > 0) + { + oids = (Oid *) palloc(nparts * sizeof(Oid)); + i = 0; + foreach (cell, partoids) + oids[i++] = lfirst_oid(cell); + + /* Convert from node to the internal representation */ + switch (key->strategy) + { + case PARTITION_STRATEGY_LIST: + { + List *non_null_values = NIL; + + /* + * Create a unified list of non-null values across all + * partitions. + */ + i = 0; + found_null = false; + null_index = -1; + foreach(cell, boundspecs) + { + ListCell *c; + PartitionBoundSpec *spec = lfirst(cell); + + if (spec->strategy != PARTITION_STRATEGY_LIST) + elog(ERROR, "invalid strategy in partition bound spec"); + + foreach (c, spec->listdatums) + { + Const *val = lfirst(c); + PartitionListValue *list_value = NULL; + + if (!val->constisnull) + { + list_value = (PartitionListValue *) + palloc0(sizeof(PartitionListValue)); + list_value->index = i; + list_value->value = val->constvalue; + } + else + { + /* + * Never put a null into the values array, flag + * instead for the code further down below where + * we construct the actual relcache struct. + */ + if (found_null) + elog(ERROR, "found null more than once"); + found_null = true; + null_index = i; + } + + if (list_value) + non_null_values = lappend(non_null_values, + list_value); + } + + i++; + } + + ndatums = list_length(non_null_values); + + /* + * Collect all list values in one array. Alongside the value, + * we also save the index of partition the value comes from. + */ + all_values = (PartitionListValue **) palloc(ndatums * + sizeof(PartitionListValue *)); + i = 0; + foreach(cell, non_null_values) + { + PartitionListValue *src = lfirst(cell); + + all_values[i] = (PartitionListValue *) + palloc(sizeof(PartitionListValue)); + all_values[i]->value = src->value; + all_values[i]->index = src->index; + i++; + } + + qsort_arg(all_values, ndatums, sizeof(PartitionListValue *), + qsort_partition_list_value_cmp, (void *) key); + break; + } + + case PARTITION_STRATEGY_RANGE: + { + int j, k; + PartitionRangeBound **all_bounds, + *prev; + bool *distinct_indexes; + + all_bounds = (PartitionRangeBound **) palloc0(2 * nparts * + sizeof(PartitionRangeBound *)); + distinct_indexes = (bool *) palloc(2 * nparts * sizeof(bool)); + + /* + * Create a unified list of range bounds across all the + * partitions. + */ + i = j = 0; + foreach(cell, boundspecs) + { + PartitionBoundSpec *spec = lfirst(cell); + PartitionRangeBound *lower, *upper; + + if (spec->strategy != PARTITION_STRATEGY_RANGE) + elog(ERROR, "invalid strategy in partition bound spec"); + + lower = make_one_range_bound(key, i, spec->lowerdatums, + true); + upper = make_one_range_bound(key, i, spec->upperdatums, + false); + all_bounds[j] = lower; + all_bounds[j+1] = upper; + j += 2; + i++; + } + Assert(j == 2 * nparts); + + /* Sort all the bounds in ascending order */ + qsort_arg(all_bounds, 2 * nparts, + sizeof(PartitionRangeBound *), + qsort_partition_rbound_cmp, + (void *) key); + /* + * Count the number of distinct bounds to allocate an array + * of that size. + */ + ndatums = 0; + prev = NULL; + for (i = 0; i < 2 * nparts; i++) + { + PartitionRangeBound *cur = all_bounds[i]; + bool is_distinct = false; + int j; + + /* Is current bound is distinct from the previous? */ + for (j = 0; j < key->partnatts; j++) + { + Datum cmpval; + + if (prev == NULL) + { + is_distinct = true; + break; + } + + /* + * If either of them has infinite element, we can't + * equate them. Even when both are infinite, they'd + * have opposite signs, because only one of cur and + * prev is a lower bound). + */ + if (cur->content[j] != RANGE_DATUM_FINITE || + prev->content[j] != RANGE_DATUM_FINITE) + { + is_distinct = true; + break; + } + cmpval = FunctionCall2Coll(&key->partsupfunc[j], + key->partcollation[j], + cur->datums[j], + prev->datums[j]); + if (DatumGetInt32(cmpval) != 0) + { + is_distinct = true; + break; + } + } + + /* + * Count the current bound if it is distinct from the + * previous one. Also, store if the index i contains + * a distinct bound that we'd like put in the relcache + * array. + */ + if (is_distinct) + { + distinct_indexes[i] = true; + ndatums++; + } + else + distinct_indexes[i] = false; + + prev = cur; + } + + /* + * Finally save them in an array from where they will be + * copied into the relcache. + */ + rbounds = (PartitionRangeBound **) palloc(ndatums * + sizeof(PartitionRangeBound *)); + k = 0; + for (i = 0; i < 2 * nparts; i++) + { + if (distinct_indexes[i]) + rbounds[k++] = all_bounds[i]; + } + Assert(k == ndatums); + break; + } + } + } + + /* Now build the actual relcache partition descriptor */ + rel->rd_pdcxt = AllocSetContextCreate(CacheMemoryContext, + RelationGetRelationName(rel), + ALLOCSET_DEFAULT_SIZES); + oldcxt = MemoryContextSwitchTo(rel->rd_pdcxt); + + result = (PartitionDescData *) palloc0(sizeof(PartitionDescData)); + result->nparts = nparts; + if (nparts > 0) + { + PartitionBoundInfo boundinfo; + int *mapping; + int next_index = 0; + + result->oids = (Oid *) palloc0(nparts * sizeof(Oid)); + + boundinfo = (PartitionBoundInfoData *) + palloc0(sizeof(PartitionBoundInfoData)); + boundinfo->strategy = key->strategy; + boundinfo->ndatums = ndatums; + boundinfo->datums = (Datum **) palloc0(ndatums * sizeof(Datum *)); + + /* Initialize mapping array with invalid values */ + mapping = (int *) palloc(sizeof(int) * nparts); + for (i = 0; i < nparts; i++) + mapping[i] = -1; + + switch (key->strategy) + { + case PARTITION_STRATEGY_LIST: + { + boundinfo->has_null = found_null; + boundinfo->indexes = (int *) palloc(ndatums * sizeof(int)); + + /* + * Copy values. Indexes of individual values are mapped to + * canonical values so that they match for any two list + * partitioned tables with same number of partitions and same + * lists per partition. One way to canonicalize is to assign + * the index in all_values[] of the smallest value of each + * partition, as the index of all of the partition's values. + */ + for (i = 0; i < ndatums; i++) + { + boundinfo->datums[i] = (Datum *) palloc(sizeof(Datum)); + boundinfo->datums[i][0] = datumCopy(all_values[i]->value, + key->parttypbyval[0], + key->parttyplen[0]); + + /* If the old index has no mapping, assign one */ + if (mapping[all_values[i]->index] == -1) + mapping[all_values[i]->index] = next_index++; + + boundinfo->indexes[i] = mapping[all_values[i]->index]; + } + + /* + * If null-accepting partition has no mapped index yet, assign + * one. This could happen if such partition accepts only null + * and hence not covered in the above loop which only handled + * non-null values. + */ + if (found_null) + { + Assert(null_index >= 0); + if (mapping[null_index] == -1) + mapping[null_index] = next_index++; + } + + /* All partition must now have a valid mapping */ + Assert(next_index == nparts); + + if (found_null) + boundinfo->null_index = mapping[null_index]; + else + boundinfo->null_index = -1; + break; + } + + case PARTITION_STRATEGY_RANGE: + { + boundinfo->content = (RangeDatumContent **) palloc(ndatums * + sizeof(RangeDatumContent *)); + boundinfo->indexes = (int *) palloc((ndatums+1) * + sizeof(int)); + + for (i = 0; i < ndatums; i++) + { + int j; + + boundinfo->datums[i] = (Datum *) palloc(key->partnatts * + sizeof(Datum)); + boundinfo->content[i] = (RangeDatumContent *) + palloc(key->partnatts * + sizeof(RangeDatumContent)); + for (j = 0; j < key->partnatts; j++) + { + if (rbounds[i]->content[j] == RANGE_DATUM_FINITE) + boundinfo->datums[i][j] = + datumCopy(rbounds[i]->datums[j], + key->parttypbyval[j], + key->parttyplen[j]); + /* Remember, we are storing the tri-state value. */ + boundinfo->content[i][j] = rbounds[i]->content[j]; + } + + /* + * There is no mapping for invalid indexes. + * + * Any lower bounds in the rbounds array have invalid + * indexes assigned, because the values between the + * previous bound (if there is one) and this (lower) + * bound are not part of the range of any existing + * partition. + */ + if (rbounds[i]->lower) + boundinfo->indexes[i] = -1; + else + { + int orig_index = rbounds[i]->index; + + /* If the old index is has no mapping, assign one */ + if (mapping[orig_index] == -1) + mapping[orig_index] = next_index++; + + boundinfo->indexes[i] = mapping[orig_index]; + } + } + boundinfo->indexes[i] = -1; + break; + } + } + + result->boundinfo = boundinfo; + + /* + * Now assign OIDs from the original array into mapped indexes + * of the result array. Order of OIDs in the former is defined + * by the catalog scan that retrived them, whereas that in the + * latter is defined by canonicalized representation of the + * list values or the range bounds. + */ + for (i = 0; i < nparts; i++) + result->oids[mapping[i]] = oids[i]; + pfree(mapping); + } + + MemoryContextSwitchTo(oldcxt); + rel->rd_partdesc = result; +} + +/* + * Are two partition bound collections logically equal? + * + * Used in the keep logic of relcache.c (ie, in RelationClearRelation()). + * This is also useful when b1 and b2 are bound collections of two separate + * relations, respectively, because PartitionBoundInfo is a canonical + * representation of partition bounds. + */ +bool +partition_bounds_equal(PartitionKey key, + PartitionBoundInfo b1, PartitionBoundInfo b2) +{ + int i; + + if (b1->strategy != b2->strategy) + return false; + + if (b1->ndatums != b2->ndatums) + return false; + + if (b1->has_null != b2->has_null) + return false; + + if (b1->null_index != b2->null_index) + return false; + + for (i = 0; i < b1->ndatums; i++) + { + int j; + + for (j = 0; j < key->partnatts; j++) + { + int32 cmpval; + + cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0], + key->partcollation[0], + b1->datums[i][j], + b2->datums[i][j])); + if (cmpval != 0) + return false; + + /* Range partitions can have infinite datums */ + if (b1->content != NULL && b1->content[i][j] != b2->content[i][j]) + return false; + } + + if(b1->indexes[i] != b2->indexes[i]) + return false; + } + + /* There are ndatums+1 indexes in case of range partitions */ + if (key->strategy == PARTITION_STRATEGY_RANGE && + b1->indexes[i] != b2->indexes[i]) + return false; + + return true; +} + +/* + * check_new_partition_bound + * + * Checks if the new partition's bound overlaps any of the existing partitions + * of parent. Also performs additional checks as necessary per strategy. + */ +void +check_new_partition_bound(char *relname, Relation parent, Node *bound) +{ + PartitionBoundSpec *spec = (PartitionBoundSpec *) bound; + PartitionKey key = RelationGetPartitionKey(parent); + PartitionDesc partdesc = RelationGetPartitionDesc(parent); + ParseState *pstate = make_parsestate(NULL); + int with = -1; + bool overlap = false; + + switch (key->strategy) + { + case PARTITION_STRATEGY_LIST: + { + Assert(spec->strategy == PARTITION_STRATEGY_LIST); + + if (partdesc->nparts > 0) + { + PartitionBoundInfo boundinfo = partdesc->boundinfo; + ListCell *cell; + + Assert(boundinfo && + boundinfo->strategy == PARTITION_STRATEGY_LIST && + (boundinfo->ndatums > 0 || boundinfo->has_null)); + + foreach (cell, spec->listdatums) + { + Const *val = lfirst(cell); + + if (!val->constisnull) + { + int offset; + bool equal; + + offset = partition_bound_bsearch(key, boundinfo, + &val->constvalue, + true, &equal); + if (offset >= 0 && equal) + { + overlap = true; + with = boundinfo->indexes[offset]; + break; + } + } + else if (boundinfo->has_null) + { + overlap = true; + with = boundinfo->null_index; + break; + } + } + } + + break; + } + + case PARTITION_STRATEGY_RANGE: + { + PartitionRangeBound *lower, + *upper; + + Assert(spec->strategy == PARTITION_STRATEGY_RANGE); + lower = make_one_range_bound(key, -1, spec->lowerdatums, true); + upper = make_one_range_bound(key, -1, spec->upperdatums, false); + + /* + * First check if the resulting range would be empty with + * specified lower and upper bounds + */ + if (partition_rbound_cmp(key, lower->datums, lower->content, true, + upper) >= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot create range partition with empty range"), + parser_errposition(pstate, spec->location))); + + if (partdesc->nparts > 0) + { + PartitionBoundInfo boundinfo = partdesc->boundinfo; + int off1, off2; + bool equal = false; + + Assert(boundinfo && boundinfo->ndatums > 0 && + boundinfo->strategy == PARTITION_STRATEGY_RANGE); + + /* + * Find the greatest index of a range bound that is less + * than or equal with the new lower bound. + */ + off1 = partition_bound_bsearch(key, boundinfo, lower, true, + &equal); + + /* + * If equal has been set to true, that means the new lower + * bound is found to be equal with the bound at off1, which + * clearly means an overlap with the partition at index + * off1+1). + * + * Otherwise, check if there is a "gap" that could be occupied + * by the new partition. In case of a gap, the new upper + * bound should not cross past the upper boundary of the gap, + * that is, off2 == off1 should be true. + */ + if (!equal && boundinfo->indexes[off1+1] < 0) + { + off2 = partition_bound_bsearch(key, boundinfo, upper, + true, &equal); + + if (equal || off1 != off2) + { + overlap = true; + with = boundinfo->indexes[off2+1]; + } + } + else + { + overlap = true; + with = boundinfo->indexes[off1+1]; + } + } + + break; + } + } + + if (overlap) + { + Assert(with >= 0); + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("partition \"%s\" would overlap partition \"%s\"", + relname, get_rel_name(partdesc->oids[with])), + parser_errposition(pstate, spec->location))); + } +} + +/* + * get_partition_parent + * + * Returns inheritance parent of a partition by scanning pg_inherits + * + * Note: Because this function assumes that the relation whose OID is passed + * as an argument will have precisely one parent, it should only be called + * when it is known that the relation is a partition. + */ +Oid +get_partition_parent(Oid relid) +{ + Form_pg_inherits form; + Relation catalogRelation; + SysScanDesc scan; + ScanKeyData key[2]; + HeapTuple tuple; + Oid result; + + catalogRelation = heap_open(InheritsRelationId, AccessShareLock); + + ScanKeyInit(&key[0], + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + ScanKeyInit(&key[1], + Anum_pg_inherits_inhseqno, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum(1)); + + scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, true, + NULL, 2, key); + + tuple = systable_getnext(scan); + Assert(HeapTupleIsValid(tuple)); + + form = (Form_pg_inherits) GETSTRUCT(tuple); + result = form->inhparent; + + systable_endscan(scan); + heap_close(catalogRelation, AccessShareLock); + + return result; +} + +/* + * get_qual_from_partbound + * Given a parser node for partition bound, return the list of executable + * expressions as partition constraint + */ +List * +get_qual_from_partbound(Relation rel, Relation parent, Node *bound) +{ + PartitionBoundSpec *spec = (PartitionBoundSpec *) bound; + PartitionKey key = RelationGetPartitionKey(parent); + List *my_qual = NIL; + TupleDesc parent_tupdesc = RelationGetDescr(parent); + AttrNumber parent_attno; + AttrNumber *partition_attnos; + bool found_whole_row; + + Assert(key != NULL); + + switch (key->strategy) + { + case PARTITION_STRATEGY_LIST: + Assert(spec->strategy == PARTITION_STRATEGY_LIST); + my_qual = get_qual_for_list(key, spec); + break; + + case PARTITION_STRATEGY_RANGE: + Assert(spec->strategy == PARTITION_STRATEGY_RANGE); + my_qual = get_qual_for_range(key, spec); + break; + } + + /* + * Translate vars in the generated expression to have correct attnos. + * Note that the vars in my_qual bear attnos dictated by key which carries + * physical attnos of the parent. We must allow for a case where physical + * attnos of a partition can be different from the parent. + */ + partition_attnos = (AttrNumber *) + palloc0(parent_tupdesc->natts * sizeof(AttrNumber)); + for (parent_attno = 1; parent_attno <= parent_tupdesc->natts; + parent_attno++) + { + Form_pg_attribute attribute = parent_tupdesc->attrs[parent_attno - 1]; + char *attname = NameStr(attribute->attname); + AttrNumber partition_attno; + + if (attribute->attisdropped) + continue; + + partition_attno = get_attnum(RelationGetRelid(rel), attname); + partition_attnos[parent_attno - 1] = partition_attno; + } + + my_qual = (List *) map_variable_attnos((Node *) my_qual, + 1, 0, + partition_attnos, + parent_tupdesc->natts, + &found_whole_row); + /* there can never be a whole-row reference here */ + if (found_whole_row) + elog(ERROR, "unexpected whole-row reference found in partition key"); + + return my_qual; +} + +/* + * RelationGetPartitionQual + * + * Returns a list of partition quals + */ +List * +RelationGetPartitionQual(Relation rel, bool recurse) +{ + /* Quick exit */ + if (!rel->rd_rel->relispartition) + return NIL; + + return generate_partition_qual(rel, recurse); +} + +/* Module-local functions */ + +/* + * get_qual_for_list + * + * Returns a list of expressions to use as a list partition's constraint. + */ +static List * +get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec) +{ + List *result; + ArrayExpr *arr; + ScalarArrayOpExpr *opexpr; + ListCell *cell, + *prev, + *next; + Node *key_col; + Oid operoid; + bool need_relabel, + list_has_null = false; + NullTest *nulltest1 = NULL, + *nulltest2 = NULL; + + /* Left operand is either a simple Var or arbitrary expression */ + if (key->partattrs[0] != 0) + key_col = (Node *) makeVar(1, + key->partattrs[0], + key->parttypid[0], + key->parttypmod[0], + key->parttypcoll[0], + 0); + else + key_col = (Node *) copyObject(linitial(key->partexprs)); + + /* + * We must remove any NULL value in the list; we handle it separately + * below. + */ + prev = NULL; + for (cell = list_head(spec->listdatums); cell; cell = next) + { + Const *val = (Const *) lfirst(cell); + + next = lnext(cell); + + if (val->constisnull) + { + list_has_null = true; + spec->listdatums = list_delete_cell(spec->listdatums, + cell, prev); + } + else + prev = cell; + } + + if (!list_has_null) + { + /* + * Gin up a col IS NOT NULL test that will be AND'd with other + * expressions + */ + nulltest1 = makeNode(NullTest); + nulltest1->arg = (Expr *) key_col; + nulltest1->nulltesttype = IS_NOT_NULL; + nulltest1->argisrow = false; + nulltest1->location = -1; + } + else + { + /* + * Gin up a col IS NULL test that will be OR'd with other expressions + */ + nulltest2 = makeNode(NullTest); + nulltest2->arg = (Expr *) key_col; + nulltest2->nulltesttype = IS_NULL; + nulltest2->argisrow = false; + nulltest2->location = -1; + } + + /* Right operand is an ArrayExpr containing this partition's values */ + arr = makeNode(ArrayExpr); + arr->array_typeid = !type_is_array(key->parttypid[0]) + ? get_array_type(key->parttypid[0]) + : key->parttypid[0]; + arr->array_collid = key->parttypcoll[0]; + arr->element_typeid = key->parttypid[0]; + arr->elements = spec->listdatums; + arr->multidims = false; + arr->location = -1; + + /* Get the correct btree equality operator */ + operoid = get_partition_operator(key, 0, BTEqualStrategyNumber, + &need_relabel); + if (need_relabel || key->partcollation[0] != key->parttypcoll[0]) + key_col = (Node *) makeRelabelType((Expr *) key_col, + key->partopcintype[0], + -1, + key->partcollation[0], + COERCE_EXPLICIT_CAST); + + /* Build leftop = ANY (rightop) */ + opexpr = makeNode(ScalarArrayOpExpr); + opexpr->opno = operoid; + opexpr->opfuncid = get_opcode(operoid); + opexpr->useOr = true; + opexpr->inputcollid = key->partcollation[0]; + opexpr->args = list_make2(key_col, arr); + opexpr->location = -1; + + if (nulltest1) + result = list_make2(nulltest1, opexpr); + else if (nulltest2) + { + Expr *or; + + or = makeBoolExpr(OR_EXPR, list_make2(nulltest2, opexpr), -1); + result = list_make1(or); + } + else + result = list_make1(opexpr); + + return result; +} + +/* + * get_qual_for_range + * + * Get a list of OpExpr's to use as a range partition's constraint. + */ +static List * +get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec) +{ + List *result = NIL; + ListCell *cell1, + *cell2, + *partexprs_item; + int i; + + /* + * Iterate over columns of the key, emitting an OpExpr for each using + * the corresponding lower and upper datums as constant operands. + */ + i = 0; + partexprs_item = list_head(key->partexprs); + forboth (cell1, spec->lowerdatums, cell2, spec->upperdatums) + { + PartitionRangeDatum *ldatum = lfirst(cell1), + *udatum = lfirst(cell2); + Node *key_col; + Const *lower_val = NULL, + *upper_val = NULL; + EState *estate; + MemoryContext oldcxt; + Expr *test_expr; + ExprState *test_exprstate; + Datum test_result; + bool isNull; + bool need_relabel = false; + Oid operoid; + + /* Left operand */ + if (key->partattrs[i] != 0) + { + key_col = (Node *) makeVar(1, + key->partattrs[i], + key->parttypid[i], + key->parttypmod[i], + key->parttypcoll[i], + 0); + } + else + { + key_col = (Node *) copyObject(lfirst(partexprs_item)); + partexprs_item = lnext(partexprs_item); + } + + /* + * Stop at this column if either of lower or upper datum is infinite, + * but do emit an OpExpr for the non-infinite datum. + */ + if (!ldatum->infinite) + lower_val = (Const *) ldatum->value; + if (!udatum->infinite) + upper_val = (Const *) udatum->value; + + /* + * If lower_val and upper_val are both finite and happen to be equal, + * emit only (key_col = lower_val) for this column, because all rows + * in this partition could only ever contain this value (ie, lower_val) + * in the current partitioning column. We must consider further + * columns because the above condition does not fully constrain the + * rows of this partition. + */ + if (lower_val && upper_val) + { + /* Get the correct btree equality operator for the test */ + operoid = get_partition_operator(key, i, BTEqualStrategyNumber, + &need_relabel); + + /* Create the test expression */ + estate = CreateExecutorState(); + oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); + test_expr = make_opclause(operoid, + BOOLOID, + false, + (Expr *) lower_val, + (Expr *) upper_val, + InvalidOid, + key->partcollation[i]); + fix_opfuncids((Node *) test_expr); + test_exprstate = ExecInitExpr(test_expr, NULL); + test_result = ExecEvalExprSwitchContext(test_exprstate, + GetPerTupleExprContext(estate), + &isNull, NULL); + MemoryContextSwitchTo(oldcxt); + FreeExecutorState(estate); + + if (DatumGetBool(test_result)) + { + /* This can never be, but it's better to make sure */ + if (i == key->partnatts - 1) + elog(ERROR, "invalid range bound specification"); + + if (need_relabel || key->partcollation[i] != key->parttypcoll[i]) + key_col = (Node *) makeRelabelType((Expr *) key_col, + key->partopcintype[i], + -1, + key->partcollation[i], + COERCE_EXPLICIT_CAST); + result = lappend(result, + make_opclause(operoid, + BOOLOID, + false, + (Expr *) key_col, + (Expr *) lower_val, + InvalidOid, + key->partcollation[i])); + + /* Go over to consider the next column. */ + i++; + continue; + } + } + + /* + * We can say here that lower_val != upper_val. Emit expressions + * (key_col >= lower_val) and (key_col < upper_val), then stop. + */ + if (lower_val) + { + operoid = get_partition_operator(key, i, + BTGreaterEqualStrategyNumber, + &need_relabel); + + if (need_relabel || key->partcollation[i] != key->parttypcoll[i]) + key_col = (Node *) makeRelabelType((Expr *) key_col, + key->partopcintype[i], + -1, + key->partcollation[i], + COERCE_EXPLICIT_CAST); + result = lappend(result, + make_opclause(operoid, + BOOLOID, + false, + (Expr *) key_col, + (Expr *) lower_val, + InvalidOid, + key->partcollation[i])); + } + + if (upper_val) + { + operoid = get_partition_operator(key, i, + BTLessStrategyNumber, + &need_relabel); + + if (need_relabel || key->partcollation[i] != key->parttypcoll[i]) + key_col = (Node *) makeRelabelType((Expr *) key_col, + key->partopcintype[i], + -1, + key->partcollation[i], + COERCE_EXPLICIT_CAST); + + result = lappend(result, + make_opclause(operoid, + BOOLOID, + false, + (Expr *) key_col, + (Expr *) upper_val, + InvalidOid, + key->partcollation[i])); + } + + /* + * We can stop at this column, because we would not have checked + * the next column when routing a given row into this partition. + */ + break; + } + + return result; +} + +/* + * get_partition_operator + * + * Return oid of the operator of given strategy for a given partition key + * column. + */ +static Oid +get_partition_operator(PartitionKey key, int col, StrategyNumber strategy, + bool *need_relabel) +{ + Oid operoid; + + /* + * First check if there exists an operator of the given strategy, with + * this column's type as both its lefttype and righttype, in the + * partitioning operator family specified for the column. + */ + operoid = get_opfamily_member(key->partopfamily[col], + key->parttypid[col], + key->parttypid[col], + strategy); + + /* + * If one doesn't exist, we must resort to using an operator in the same + * opreator family but with the operator class declared input type. It is + * OK to do so, because the column's type is known to be binary-coercible + * with the operator class input type (otherwise, the operator class in + * question would not have been accepted as the partitioning operator + * class). We must however inform the caller to wrap the non-Const + * expression with a RelabelType node to denote the implicit coercion. It + * ensures that the resulting expression structurally matches similarly + * processed expressions within the optimizer. + */ + if (!OidIsValid(operoid)) + { + operoid = get_opfamily_member(key->partopfamily[col], + key->partopcintype[col], + key->partopcintype[col], + strategy); + *need_relabel = true; + } + else + *need_relabel = false; + + if (!OidIsValid(operoid)) + elog(ERROR, "could not find operator for partitioning"); + + return operoid; +} + +/* + * generate_partition_qual + * + * Generate partition predicate from rel's partition bound expression + * + * Result expression tree is stored CacheMemoryContext to ensure it survives + * as long as the relcache entry. But we should be running in a less long-lived + * working context. To avoid leaking cache memory if this routine fails partway + * through, we build in working memory and then copy the completed structure + * into cache memory. + */ +static List * +generate_partition_qual(Relation rel, bool recurse) +{ + HeapTuple tuple; + MemoryContext oldcxt; + Datum boundDatum; + bool isnull; + Node *bound; + List *my_qual = NIL, + *result = NIL; + Relation parent; + + /* Guard against stack overflow due to overly deep partition tree */ + check_stack_depth(); + + /* Grab at least an AccessShareLock on the parent table */ + parent = heap_open(get_partition_parent(RelationGetRelid(rel)), + AccessShareLock); + + /* Quick copy */ + if (rel->rd_partcheck) + { + if (parent->rd_rel->relispartition && recurse) + result = list_concat(generate_partition_qual(parent, true), + copyObject(rel->rd_partcheck)); + else + result = copyObject(rel->rd_partcheck); + + heap_close(parent, AccessShareLock); + return result; + } + + /* Get pg_class.relpartbound */ + if (!rel->rd_rel->relispartition) /* should not happen */ + elog(ERROR, "relation \"%s\" has relispartition = false", + RelationGetRelationName(rel)); + tuple = SearchSysCache1(RELOID, RelationGetRelid(rel)); + boundDatum = SysCacheGetAttr(RELOID, tuple, + Anum_pg_class_relpartbound, + &isnull); + if (isnull) /* should not happen */ + elog(ERROR, "relation \"%s\" has relpartbound = null", + RelationGetRelationName(rel)); + bound = stringToNode(TextDatumGetCString(boundDatum)); + ReleaseSysCache(tuple); + + my_qual = get_qual_from_partbound(rel, parent, bound); + + /* If requested, add parent's quals to the list (if any) */ + if (parent->rd_rel->relispartition && recurse) + { + List *parent_check; + + parent_check = generate_partition_qual(parent, true); + result = list_concat(parent_check, my_qual); + } + else + result = my_qual; + + /* Save a copy of my_qual in the relcache */ + oldcxt = MemoryContextSwitchTo(CacheMemoryContext); + rel->rd_partcheck = copyObject(my_qual); + MemoryContextSwitchTo(oldcxt); + + /* Keep the parent locked until commit */ + heap_close(parent, NoLock); + + return result; +} + +/* + * qsort_partition_list_value_cmp + * + * Compare two list partition bound datums + */ +static int32 +qsort_partition_list_value_cmp(const void *a, const void *b, void *arg) +{ + Datum val1 = (*(const PartitionListValue **) a)->value, + val2 = (*(const PartitionListValue **) b)->value; + PartitionKey key = (PartitionKey) arg; + + return DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0], + key->partcollation[0], + val1, val2)); +} + +/* + * make_one_range_bound + * + * Return a PartitionRangeBound given a list of PartitionRangeDatum elements + * and a flag telling whether the bound is lower or not. Made into a function + * because there are multiple sites that want to use this facility. + */ +static PartitionRangeBound * +make_one_range_bound(PartitionKey key, int index, List *datums, bool lower) +{ + PartitionRangeBound *bound; + ListCell *cell; + int i; + + bound = (PartitionRangeBound *) palloc0(sizeof(PartitionRangeBound)); + bound->index = index; + bound->datums = (Datum *) palloc0(key->partnatts * sizeof(Datum)); + bound->content = (RangeDatumContent *) palloc0(key->partnatts * + sizeof(RangeDatumContent)); + bound->lower = lower; + + i = 0; + foreach (cell, datums) + { + PartitionRangeDatum *datum = lfirst(cell); + + /* What's contained in this range datum? */ + bound->content[i] = !datum->infinite + ? RANGE_DATUM_FINITE + : (lower ? RANGE_DATUM_NEG_INF + : RANGE_DATUM_POS_INF); + + if (bound->content[i] == RANGE_DATUM_FINITE) + { + Const *val = (Const *) datum->value; + + if (val->constisnull) + elog(ERROR, "invalid range bound datum"); + bound->datums[i] = val->constvalue; + } + + i++; + } + + return bound; +} + +/* Used when sorting range bounds across all range partitions */ +static int32 +qsort_partition_rbound_cmp(const void *a, const void *b, void *arg) +{ + PartitionRangeBound *b1 = (*(PartitionRangeBound *const *) a); + PartitionRangeBound *b2 = (*(PartitionRangeBound *const *) b); + PartitionKey key = (PartitionKey) arg; + + return partition_rbound_cmp(key, b1->datums, b1->content, b1->lower, b2); +} + +/* + * partition_rbound_cmp + * + * Return for two range bounds whether the 1st one (specified in datum1, + * content1, and lower1) is <=, =, >= the bound specified in *b2 + */ +static int32 +partition_rbound_cmp(PartitionKey key, + Datum *datums1, RangeDatumContent *content1, bool lower1, + PartitionRangeBound *b2) +{ + int32 cmpval; + int i; + Datum *datums2 = b2->datums; + RangeDatumContent *content2 = b2->content; + bool lower2 = b2->lower; + + for (i = 0; i < key->partnatts; i++) + { + /* + * First, handle cases involving infinity, which don't require + * invoking the comparison proc. + */ + if (content1[i] != RANGE_DATUM_FINITE && + content2[i] != RANGE_DATUM_FINITE) + /* + * Both are infinity, so they are equal unless one is + * negative infinity and other positive (or vice versa) + */ + return content1[i] == content2[i] ? 0 + : (content1[i] < content2[i] ? -1 : 1); + else if (content1[i] != RANGE_DATUM_FINITE) + return content1[i] == RANGE_DATUM_NEG_INF ? -1 : 1; + else if (content2[i] != RANGE_DATUM_FINITE) + return content2[i] == RANGE_DATUM_NEG_INF ? 1 : -1; + + cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[i], + key->partcollation[i], + datums1[i], + datums2[i])); + if (cmpval != 0) + break; + } + + /* + * If the comparison is anything other than equal, we're done. If + * they compare equal though, we still have to consider whether + * the boundaries are inclusive or exclusive. Exclusive one is + * considered smaller of the two. + */ + if (cmpval == 0 && lower1 != lower2) + cmpval = lower1 ? 1 : -1; + + return cmpval; +} + +/* + * partition_bound_cmp + * + * Return whether the bound at offset in boundinfo is <=, =, >= the argument + * specified in *probe. + */ +static int32 +partition_bound_cmp(PartitionKey key, PartitionBoundInfo boundinfo, + int offset, void *probe, bool probe_is_bound) +{ + Datum *bound_datums = boundinfo->datums[offset]; + int32 cmpval; + + switch (key->strategy) + { + case PARTITION_STRATEGY_LIST: + cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0], + key->partcollation[0], + bound_datums[0], + *(Datum *) probe)); + break; + + case PARTITION_STRATEGY_RANGE: + { + RangeDatumContent *content = boundinfo->content[offset]; + + if (probe_is_bound) + { + /* + * We need to pass whether the existing bound is a lower + * bound, so that two equal-valued lower and upper bounds are + * not regarded equal. + */ + bool lower = boundinfo->indexes[offset] < 0; + + cmpval = partition_rbound_cmp(key, + bound_datums, content, lower, + (PartitionRangeBound *) probe); + } + + break; + } + } + + return cmpval; +} + +/* + * Binary search on a collection of partition bounds. Returns greatest index + * of bound in array boundinfo->datums which is less or equal with *probe. + * If all bounds in the array are greater than *probe, -1 is returned. + * + * *probe could either be a partition bound or a Datum array representing + * the partition key of a tuple being routed; probe_is_bound tells which. + * We pass that down to the comparison function so that it can interpret the + * contents of *probe accordingly. + * + * *is_equal is set to whether the bound at the returned index is equal with + * *probe. + */ +static int +partition_bound_bsearch(PartitionKey key, PartitionBoundInfo boundinfo, + void *probe, bool probe_is_bound, bool *is_equal) +{ + int lo, + hi, + mid; + + lo = -1; + hi = boundinfo->ndatums - 1; + while (lo < hi) + { + int32 cmpval; + + mid = (lo + hi + 1) / 2; + cmpval = partition_bound_cmp(key, boundinfo, mid, probe, + probe_is_bound); + if (cmpval <= 0) + { + lo = mid; + *is_equal = (cmpval == 0); + } + else + hi = mid - 1; + } + + return lo; +} diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index 5b4f6af..d6d52d9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -112,7 +112,7 @@ create_ctas_internal(List *attrList, IntoClause *into) * Create the relation. (This will error out if there's an existing view, * so we don't need more code to complain if "replace" is false.) */ - intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL); + intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL); /* * If necessary, create a TOAST table for the target table. Note that diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c index 1ab9030..d953b44 100644 --- a/src/backend/commands/sequence.c +++ b/src/backend/commands/sequence.c @@ -234,7 +234,7 @@ DefineSequence(ParseState *pstate, CreateSeqStmt *seq) stmt->tablespacename = NULL; stmt->if_not_exists = seq->if_not_exists; - address = DefineRelation(stmt, RELKIND_SEQUENCE, seq->ownerId, NULL); + address = DefineRelation(stmt, RELKIND_SEQUENCE, seq->ownerId, NULL, NULL); seqoid = address.objectId; Assert(seqoid != InvalidOid); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 7e2beff..99c9d10 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -29,6 +29,7 @@ #include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/objectaccess.h" +#include "catalog/partition.h" #include "catalog/pg_am.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" @@ -65,6 +66,8 @@ #include "nodes/parsenodes.h" #include "optimizer/clauses.h" #include "optimizer/planner.h" +#include "optimizer/predtest.h" +#include "optimizer/prep.h" #include "optimizer/var.h" #include "parser/parse_clause.h" #include "parser/parse_coerce.h" @@ -163,6 +166,7 @@ typedef struct AlteredTableInfo Oid newTableSpace; /* new tablespace; 0 means no change */ bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */ char newrelpersistence; /* if above is true */ + List *partition_constraint; /* for attach partition validation */ /* Objects to rebuild after completing ALTER TYPE operations */ List *changedConstraintOids; /* OIDs of constraints to rebuild */ List *changedConstraintDefs; /* string definitions of same */ @@ -279,7 +283,8 @@ struct DropRelationCallbackState static void truncate_check_rel(Relation rel); static List *MergeAttributes(List *schema, List *supers, char relpersistence, - List **supOids, List **supconstr, int *supOidCount); + bool is_partition, List **supOids, List **supconstr, + int *supOidCount); static bool MergeCheckConstraint(List *constraints, char *name, Node *expr); static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel); static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel); @@ -346,7 +351,9 @@ static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid); static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid); static void ATPrepAddOids(List **wqueue, Relation rel, bool recurse, AlterTableCmd *cmd, LOCKMODE lockmode); +static void ATPrepDropNotNull(Relation rel, bool recurse, bool recursing); static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode); +static void ATPrepSetNotNull(Relation rel, bool recurse, bool recursing); static ObjectAddress ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, const char *colName, LOCKMODE lockmode); static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName, @@ -444,6 +451,11 @@ static bool is_partition_attr(Relation rel, AttrNumber attnum, bool *used_in_exp static PartitionSpec *transformPartitionSpec(Relation rel, PartitionSpec *partspec, char *strategy); static void ComputePartitionAttrs(Relation rel, List *partParams, AttrNumber *partattrs, List **partexprs, Oid *partopclass, Oid *partcollation); +static void CreateInheritance(Relation child_rel, Relation parent_rel); +static void RemoveInheritance(Relation child_rel, Relation parent_rel); +static ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel, + PartitionCmd *cmd); +static ObjectAddress ATExecDetachPartition(Relation rel, RangeVar *name); /* ---------------------------------------------------------------- @@ -466,7 +478,7 @@ static void ComputePartitionAttrs(Relation rel, List *partParams, AttrNumber *pa */ ObjectAddress DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, - ObjectAddress *typaddress) + ObjectAddress *typaddress, const char *queryString) { char relname[NAMEDATALEN]; Oid namespaceId; @@ -597,6 +609,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, */ schema = MergeAttributes(schema, stmt->inhRelations, stmt->relation->relpersistence, + stmt->partbound != NULL, &inheritOids, &old_constraints, &parentOidCount); /* @@ -607,18 +620,33 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, descriptor = BuildDescForRelation(schema); /* - * Notice that we allow OIDs here only for plain tables, even though some - * other relkinds can support them. This is necessary because the - * default_with_oids GUC must apply only to plain tables and not any other - * relkind; doing otherwise would break existing pg_dump files. We could - * allow explicit "WITH OIDS" while not allowing default_with_oids to - * affect other relkinds, but it would complicate interpretOidsOption(). + * Notice that we allow OIDs here only for plain tables and partitioned + * tables, even though some other relkinds can support them. This is + * necessary because the default_with_oids GUC must apply only to plain + * tables and not any other relkind; doing otherwise would break existing + * pg_dump files. We could allow explicit "WITH OIDS" while not allowing + * default_with_oids to affect other relkinds, but it would complicate + * interpretOidsOption(). */ localHasOids = interpretOidsOption(stmt->options, (relkind == RELKIND_RELATION || relkind == RELKIND_PARTITIONED_TABLE)); descriptor->tdhasoid = (localHasOids || parentOidCount > 0); + if (stmt->partbound) + { + /* If the parent has OIDs, partitions must have them too. */ + if (parentOidCount > 0 && !localHasOids) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create table without OIDs as partition of table with OIDs"))); + /* If the parent doesn't, partitions must not have them. */ + if (parentOidCount == 0 && localHasOids) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create table with OIDs as partition of table without OIDs"))); + } + /* * Find columns with default values and prepare for insertion of the * defaults. Pre-cooked (that is, inherited) defaults go into a list of @@ -717,6 +745,51 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, */ rel = relation_open(relationId, AccessExclusiveLock); + /* Process and store partition bound, if any. */ + if (stmt->partbound) + { + Node *bound; + ParseState *pstate; + Oid parentId = linitial_oid(inheritOids); + Relation parent; + + /* Already have strong enough lock on the parent */ + parent = heap_open(parentId, NoLock); + + /* + * We are going to try to validate the partition bound specification + * against the partition key of parentRel, so it better have one. + */ + if (parent->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("\"%s\" is not partitioned", + RelationGetRelationName(parent)))); + + /* Tranform the bound values */ + pstate = make_parsestate(NULL); + pstate->p_sourcetext = queryString; + bound = transformPartitionBound(pstate, parent, stmt->partbound); + + /* + * Check first that the new partition's bound is valid and does not + * overlap with any of existing partitions of the parent - note that + * it does not return on error. + */ + check_new_partition_bound(relname, parent, bound); + heap_close(parent, NoLock); + + /* Update the pg_class entry. */ + StorePartitionBound(rel, bound); + + /* + * The code that follows may also update the pg_class tuple to update + * relnumchecks, so bump up the command counter to avoid the "already + * updated by self" error. + */ + CommandCounterIncrement(); + } + /* * Process the partitioning specification (if any) and store the * partition key information into the catalog. @@ -1146,6 +1219,10 @@ ExecuteTruncate(TruncateStmt *stmt) relids = lappend_oid(relids, childrelid); } } + else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("must truncate child tables too"))); } /* @@ -1452,6 +1529,7 @@ storage_name(char c) * of ColumnDef's.) It is destructively changed. * 'supers' is a list of names (as RangeVar nodes) of parent relations. * 'relpersistence' is a persistence type of the table. + * 'is_partition' tells if the table is a partition * * Output arguments: * 'supOids' receives a list of the OIDs of the parent relations. @@ -1503,7 +1581,8 @@ storage_name(char c) */ static List * MergeAttributes(List *schema, List *supers, char relpersistence, - List **supOids, List **supconstr, int *supOidCount) + bool is_partition, List **supOids, List **supconstr, + int *supOidCount) { ListCell *entry; List *inhSchema = NIL; @@ -1513,6 +1592,7 @@ MergeAttributes(List *schema, List *supers, char relpersistence, bool have_bogus_defaults = false; int child_attno; static Node bogus_marker = {0}; /* marks conflicting defaults */ + List *saved_schema = NIL; /* * Check for and reject tables with too many columns. We perform this @@ -1532,6 +1612,18 @@ MergeAttributes(List *schema, List *supers, char relpersistence, MaxHeapAttributeNumber))); /* + * In case of a partition, there are no new column definitions, only + * column options specified using the WITH OPTIONS clauses. We merge + * those options with actual column definitions after we have finished + * generating them from the parent's schema. + */ + if (is_partition) + { + saved_schema = schema; + schema = NIL; + } + + /* * Check for duplicate names in the explicit list of attributes. * * Although we might consider merging such entries in the same way that we @@ -1611,18 +1703,35 @@ MergeAttributes(List *schema, List *supers, char relpersistence, * on the parent table, which might otherwise be attempting to clear * the parent's relhassubclass field, if its previous children were * recently dropped. + * + * If the child table is a partition, then we instead grab an exclusive + * lock on the parent because its partition descriptor will be changed + * by addition of the new partition. */ - relation = heap_openrv(parent, ShareUpdateExclusiveLock); + if (!is_partition) + relation = heap_openrv(parent, ShareUpdateExclusiveLock); + else + relation = heap_openrv(parent, AccessExclusiveLock); - /* Cannot inherit from partitioned tables */ - if (relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + /* + * We do not allow partitioned tables and partitions to participate + * in regular inheritance. + */ + if (relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && + !is_partition) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot inherit from partitioned table \"%s\"", parent->relname))); + if (relation->rd_rel->relispartition && !is_partition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot inherit from partition \"%s\"", + parent->relname))); if (relation->rd_rel->relkind != RELKIND_RELATION && - relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE) + relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE && + relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("inherited relation \"%s\" is not a table or foreign table", @@ -1632,7 +1741,9 @@ MergeAttributes(List *schema, List *supers, char relpersistence, relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot inherit from temporary relation \"%s\"", + errmsg(!is_partition + ? "cannot inherit from temporary relation \"%s\"" + : "cannot create as partition of temporary relation \"%s\"", parent->relname))); /* If existing rel is temp, it must belong to this session */ @@ -1640,7 +1751,9 @@ MergeAttributes(List *schema, List *supers, char relpersistence, !relation->rd_islocaltemp) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot inherit from temporary relation of another session"))); + errmsg(!is_partition + ? "cannot inherit from temporary relation of another session" + : "cannot create as partition of temporary relation of another session"))); /* * We should have an UNDER permission flag for this, but for now, @@ -1887,7 +2000,8 @@ MergeAttributes(List *schema, List *supers, char relpersistence, /* * If we had no inherited attributes, the result schema is just the * explicitly declared columns. Otherwise, we need to merge the declared - * columns into the inherited schema list. + * columns into the inherited schema list. Although, we never have any + * explicitly declared columns if the table is a partition. */ if (inhSchema != NIL) { @@ -1916,6 +2030,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence, newcollid; /* + * Partitions have only one parent, so conflict should never + * occur + */ + Assert(!is_partition); + + /* * Yes, try to merge the two column definitions. They must * have the same type, typmod, and collation. */ @@ -1997,6 +2117,56 @@ MergeAttributes(List *schema, List *supers, char relpersistence, } /* + * Now that we have the column definition list for a partition, we can + * check whether the columns referenced in column option specifications + * actually exist. Also, we merge the options into the corresponding + * column definitions. + */ + if (is_partition && list_length(saved_schema) > 0) + { + schema = list_concat(schema, saved_schema); + + foreach(entry, schema) + { + ColumnDef *coldef = lfirst(entry); + ListCell *rest = lnext(entry); + ListCell *prev = entry; + + /* + * Partition column option that does not belong to a column from + * the parent. This works because the columns from the parent + * come first in the list (see above). + */ + if (coldef->typeName == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" does not exist", + coldef->colname))); + while (rest != NULL) + { + ColumnDef *restdef = lfirst(rest); + ListCell *next = lnext(rest); /* need to save it in case + * we delete it */ + + if (strcmp(coldef->colname, restdef->colname) == 0) + { + /* + * merge the column options into the column from the + * parent + */ + coldef->is_not_null = restdef->is_not_null; + coldef->raw_default = restdef->raw_default; + coldef->cooked_default = restdef->cooked_default; + coldef->constraints = restdef->constraints; + list_delete_cell(schema, rest, prev); + } + prev = rest; + rest = next; + } + } + } + + /* * If we found any conflicting parent default values, check to make sure * they were overridden by the child. */ @@ -3158,6 +3328,11 @@ AlterTableGetLockLevel(List *cmds) cmd_lockmode = AlterTableGetRelOptionsLockLevel((List *) cmd->def); break; + case AT_AttachPartition: + case AT_DetachPartition: + cmd_lockmode = AccessExclusiveLock; + break; + default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -3269,12 +3444,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, break; case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE); + ATPrepDropNotNull(rel, recurse, recursing); ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode); /* No command-specific prep needed */ pass = AT_PASS_DROP; break; case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */ ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE); + ATPrepSetNotNull(rel, recurse, recursing); ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode); /* No command-specific prep needed */ pass = AT_PASS_ADD_CONSTR; @@ -3475,6 +3652,12 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, /* No command-specific prep needed */ pass = AT_PASS_MISC; break; + case AT_AttachPartition: + case AT_DetachPartition: + ATSimplePermissions(rel, ATT_TABLE); + /* No command-specific prep needed */ + pass = AT_PASS_MISC; + break; default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -3545,7 +3728,14 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode) { AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab); - if (tab->relkind == RELKIND_RELATION || + /* + * If the table is source table of ATTACH PARTITION command, we did + * not modify anything about it that will change its toasting + * requirement, so no need to check. + */ + if (((tab->relkind == RELKIND_RELATION || + tab->relkind == RELKIND_PARTITIONED_TABLE) && + tab->partition_constraint == NIL) || tab->relkind == RELKIND_MATVIEW) AlterTableCreateToastTable(tab->relid, (Datum) 0, lockmode); } @@ -3794,6 +3984,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, case AT_GenericOptions: ATExecGenericOptions(rel, (List *) cmd->def); break; + case AT_AttachPartition: + ATExecAttachPartition(wqueue, rel, (PartitionCmd *) cmd->def); + break; + case AT_DetachPartition: + ATExecDetachPartition(rel, ((PartitionCmd *) cmd->def)->name); + break; default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -3979,7 +4175,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode) * Test the current data within the table against new constraints * generated by ALTER TABLE commands, but don't rebuild data. */ - if (tab->constraints != NIL || tab->new_notnull) + if (tab->constraints != NIL || tab->new_notnull || + tab->partition_constraint != NIL) ATRewriteTable(tab, InvalidOid, lockmode); /* @@ -4059,6 +4256,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode) CommandId mycid; BulkInsertState bistate; int hi_options; + List *partqualstate = NIL; /* * Open the relation(s). We have surely already locked the existing @@ -4123,6 +4321,15 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode) } } + /* Build expression execution states for partition check quals */ + if (tab->partition_constraint) + { + needscan = true; + partqualstate = (List *) + ExecPrepareExpr((Expr *) tab->partition_constraint, + estate); + } + foreach(l, tab->newvals) { NewColumnValue *ex = lfirst(l); @@ -4312,6 +4519,11 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode) } } + if (partqualstate && !ExecQual(partqualstate, econtext, true)) + ereport(ERROR, + (errcode(ERRCODE_CHECK_VIOLATION), + errmsg("partition constraint is violated by some row"))); + /* Write the tuple out to the new relation */ if (newrel) heap_insert(newrel, tuple, mycid, hi_options, bistate); @@ -4509,7 +4721,8 @@ ATSimpleRecursion(List **wqueue, Relation rel, */ if (recurse && (rel->rd_rel->relkind == RELKIND_RELATION || - rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)) + rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE || + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)) { Oid relid = RelationGetRelid(rel); ListCell *child; @@ -4831,6 +5044,11 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, if (recursing) ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE); + if (rel->rd_rel->relispartition && !recursing) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot add column to a partition"))); + attrdesc = heap_open(AttributeRelationId, RowExclusiveLock); /* @@ -5277,6 +5495,20 @@ ATPrepAddOids(List **wqueue, Relation rel, bool recurse, AlterTableCmd *cmd, LOC * Return the address of the modified column. If the column was already * nullable, InvalidObjectAddress is returned. */ + +static void +ATPrepDropNotNull(Relation rel, bool recurse, bool recursing) +{ + /* + * If the parent is a partitioned table, like check constraints, NOT NULL + * constraints must be dropped from child tables. + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && + !recurse && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be dropped from child tables too"))); +} static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) { @@ -5352,6 +5584,23 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) list_free(indexoidlist); + /* If rel is partition, shouldn't drop NOT NULL if parent has the same */ + if (rel->rd_rel->relispartition) + { + Oid parentId = get_partition_parent(RelationGetRelid(rel)); + Relation parent = heap_open(parentId, AccessShareLock); + TupleDesc tupDesc = RelationGetDescr(parent); + AttrNumber parent_attnum; + + parent_attnum = get_attnum(parentId, colName); + if (tupDesc->attrs[parent_attnum - 1]->attnotnull) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is marked NOT NULL in parent table", + colName))); + heap_close(parent, AccessShareLock); + } + /* * If the table is a range partitioned table, check that the column * is not in the partition key. @@ -5406,6 +5655,21 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) * Return the address of the modified column. If the column was already NOT * NULL, InvalidObjectAddress is returned. */ + +static void +ATPrepSetNotNull(Relation rel, bool recurse, bool recursing) +{ + /* + * If the parent is a partitioned table, like check constraints, NOT NULL + * constraints must be added to the child tables. + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && + !recurse && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be added to child tables too"))); +} + static ObjectAddress ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, const char *colName, LOCKMODE lockmode) @@ -5965,6 +6229,15 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName, Relation attr_rel; ListCell *child; + /* + * In case of a partitioned table, the column must be dropped from the + * partitions as well. + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && !recurse) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column must be dropped from child tables too"))); + attr_rel = heap_open(AttributeRelationId, RowExclusiveLock); foreach(child, children) { @@ -7967,6 +8240,16 @@ ATExecDropConstraint(Relation rel, const char *constrName, } /* + * In case of a partitioned table, the constraint must be dropped from + * the partitions too. There is no such thing as NO INHERIT constraints + * in case of partitioned tables. + */ + if (!recurse && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be dropped from child tables too"))); + + /* * Propagate to children as appropriate. Unlike most other ALTER * routines, we have to do this one level of recursion at a time; we can't * use find_all_inheritors to do it in one pass. @@ -10268,6 +10551,11 @@ ATPrepAddInherit(Relation child_rel) (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot change inheritance of typed table"))); + if (child_rel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change inheritance of a partition"))); + if (child_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -10280,12 +10568,7 @@ ATPrepAddInherit(Relation child_rel) static ObjectAddress ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) { - Relation parent_rel, - catalogRelation; - SysScanDesc scan; - ScanKeyData key; - HeapTuple inheritsTuple; - int32 inhseqno; + Relation parent_rel; List *children; ObjectAddress address; @@ -10330,37 +10613,11 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) errmsg("cannot inherit from partitioned table \"%s\"", parent->relname))); - /* - * Check for duplicates in the list of parents, and determine the highest - * inhseqno already present; we'll use the next one for the new parent. - * (Note: get RowExclusiveLock because we will write pg_inherits below.) - * - * Note: we do not reject the case where the child already inherits from - * the parent indirectly; CREATE TABLE doesn't reject comparable cases. - */ - catalogRelation = heap_open(InheritsRelationId, RowExclusiveLock); - ScanKeyInit(&key, - Anum_pg_inherits_inhrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(child_rel))); - scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, - true, NULL, 1, &key); - - /* inhseqno sequences start at 1 */ - inhseqno = 0; - while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) - { - Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); - - if (inh->inhparent == RelationGetRelid(parent_rel)) - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" would be inherited from more than once", - RelationGetRelationName(parent_rel)))); - if (inh->inhseqno > inhseqno) - inhseqno = inh->inhseqno; - } - systable_endscan(scan); + /* Likewise for partitions */ + if (parent_rel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot inherit from a partition"))); /* * Prevent circularity by seeing if proposed parent inherits from child. @@ -10395,6 +10652,69 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) RelationGetRelationName(child_rel), RelationGetRelationName(parent_rel)))); + /* OK to create inheritance */ + CreateInheritance(child_rel, parent_rel); + + ObjectAddressSet(address, RelationRelationId, + RelationGetRelid(parent_rel)); + + /* keep our lock on the parent relation until commit */ + heap_close(parent_rel, NoLock); + + return address; +} + +/* + * CreateInheritance + * Catalog manipulation portion of creating inheritance between a child + * table and a parent table. + * + * Common to ATExecAddInherit() and ATExecAttachPartition(). + */ +static void +CreateInheritance(Relation child_rel, Relation parent_rel) +{ + Relation catalogRelation; + SysScanDesc scan; + ScanKeyData key; + HeapTuple inheritsTuple; + int32 inhseqno; + + /* Note: get RowExclusiveLock because we will write pg_inherits below. */ + catalogRelation = heap_open(InheritsRelationId, RowExclusiveLock); + + /* + * Check for duplicates in the list of parents, and determine the highest + * inhseqno already present; we'll use the next one for the new parent. + * Also, if proposed child is a partition, it cannot already be inheriting. + * + * Note: we do not reject the case where the child already inherits from + * the parent indirectly; CREATE TABLE doesn't reject comparable cases. + */ + ScanKeyInit(&key, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(child_rel))); + scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, + true, NULL, 1, &key); + + /* inhseqno sequences start at 1 */ + inhseqno = 0; + while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) + { + Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); + + if (inh->inhparent == RelationGetRelid(parent_rel)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" would be inherited from more than once", + RelationGetRelationName(parent_rel)))); + + if (inh->inhseqno > inhseqno) + inhseqno = inh->inhseqno; + } + systable_endscan(scan); + /* Match up the columns and bump attinhcount as needed */ MergeAttributesIntoExisting(child_rel, parent_rel); @@ -10409,16 +10729,8 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) inhseqno + 1, catalogRelation); - ObjectAddressSet(address, RelationRelationId, - RelationGetRelid(parent_rel)); - /* Now we're done with pg_inherits */ heap_close(catalogRelation, RowExclusiveLock); - - /* keep our lock on the parent relation until commit */ - heap_close(parent_rel, NoLock); - - return address; } /* @@ -10469,7 +10781,7 @@ constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc) * Check columns in child table match up with columns in parent, and increment * their attinhcount. * - * Called by ATExecAddInherit + * Called by CreateInheritance * * Currently all parent columns must be found in child. Missing columns are an * error. One day we might consider creating new columns like CREATE TABLE @@ -10487,12 +10799,17 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel) int parent_natts; TupleDesc tupleDesc; HeapTuple tuple; + bool child_is_partition = false; attrrel = heap_open(AttributeRelationId, RowExclusiveLock); tupleDesc = RelationGetDescr(parent_rel); parent_natts = tupleDesc->natts; + /* If parent_rel is a partitioned table, child_rel must be a partition */ + if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + child_is_partition = true; + for (parent_attno = 1; parent_attno <= parent_natts; parent_attno++) { Form_pg_attribute attribute = tupleDesc->attrs[parent_attno - 1]; @@ -10540,6 +10857,18 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel) * later on, this change will just roll back.) */ childatt->attinhcount++; + + /* + * In case of partitions, we must enforce that value of attislocal + * is same in all partitions. (Note: there are only inherited + * attributes in partitions) + */ + if (child_is_partition) + { + Assert(childatt->attinhcount == 1); + childatt->attislocal = false; + } + simple_heap_update(attrrel, &tuple->t_self, tuple); CatalogUpdateIndexes(attrrel, tuple); heap_freetuple(tuple); @@ -10562,7 +10891,7 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel) * * Constraints that are marked ONLY in the parent are ignored. * - * Called by ATExecAddInherit + * Called by CreateInheritance * * Currently all constraints in parent must be present in the child. One day we * may consider adding new constraints like CREATE TABLE does. @@ -10581,10 +10910,15 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) SysScanDesc parent_scan; ScanKeyData parent_key; HeapTuple parent_tuple; + bool child_is_partition = false; catalog_relation = heap_open(ConstraintRelationId, RowExclusiveLock); tuple_desc = RelationGetDescr(catalog_relation); + /* If parent_rel is a partitioned table, child_rel must be a partition */ + if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + child_is_partition = true; + /* Outer loop scans through the parent's constraint definitions */ ScanKeyInit(&parent_key, Anum_pg_constraint_conrelid, @@ -10661,6 +10995,18 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) child_copy = heap_copytuple(child_tuple); child_con = (Form_pg_constraint) GETSTRUCT(child_copy); child_con->coninhcount++; + + /* + * In case of partitions, an inherited constraint must be + * inherited only once since it cannot have multiple parents and + * it is never considered local. + */ + if (child_is_partition) + { + Assert(child_con->coninhcount == 1); + child_con->conislocal = false; + } + simple_heap_update(catalog_relation, &child_copy->t_self, child_copy); CatalogUpdateIndexes(catalog_relation, child_copy); heap_freetuple(child_copy); @@ -10685,6 +11031,46 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) /* * ALTER TABLE NO INHERIT * + * Return value is the address of the relation that is no longer parent. + */ +static ObjectAddress +ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) +{ + ObjectAddress address; + Relation parent_rel; + + if (rel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change inheritance of a partition"))); + + /* + * AccessShareLock on the parent is probably enough, seeing that DROP + * TABLE doesn't lock parent tables at all. We need some lock since we'll + * be inspecting the parent's schema. + */ + parent_rel = heap_openrv(parent, AccessShareLock); + + /* + * We don't bother to check ownership of the parent table --- ownership of + * the child is presumed enough rights. + */ + + /* Off to RemoveInheritance() where most of the work happens */ + RemoveInheritance(rel, parent_rel); + + /* keep our lock on the parent relation until commit */ + heap_close(parent_rel, NoLock); + + ObjectAddressSet(address, RelationRelationId, + RelationGetRelid(parent_rel)); + + return address; +} + +/* + * RemoveInheritance + * * Drop a parent from the child's parents. This just adjusts the attinhcount * and attislocal of the columns and removes the pg_inherit and pg_depend * entries. @@ -10698,13 +11084,11 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) * coninhcount and conislocal for inherited constraints are adjusted in * exactly the same way. * - * Return value is the address of the relation that is no longer parent. + * Common to ATExecDropInherit() and ATExecDetachPartition(). */ -static ObjectAddress -ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) +static void +RemoveInheritance(Relation child_rel, Relation parent_rel) { - Relation parent_rel; - Oid parent_oid; Relation catalogRelation; SysScanDesc scan; ScanKeyData key[3]; @@ -10713,19 +11097,11 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) constraintTuple; List *connames; bool found = false; - ObjectAddress address; - - /* - * AccessShareLock on the parent is probably enough, seeing that DROP - * TABLE doesn't lock parent tables at all. We need some lock since we'll - * be inspecting the parent's schema. - */ - parent_rel = heap_openrv(parent, AccessShareLock); + bool child_is_partition = false; - /* - * We don't bother to check ownership of the parent table --- ownership of - * the child is presumed enough rights. - */ + /* If parent_rel is a partitioned table, child_rel must be a partition */ + if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + child_is_partition = true; /* * Find and destroy the pg_inherits entry linking the two, or error out if @@ -10735,7 +11111,7 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) ScanKeyInit(&key[0], Anum_pg_inherits_inhrelid, BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(rel))); + ObjectIdGetDatum(RelationGetRelid(child_rel))); scan = systable_beginscan(catalogRelation, InheritsRelidSeqnoIndexId, true, NULL, 1, key); @@ -10756,11 +11132,20 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) heap_close(catalogRelation, RowExclusiveLock); if (!found) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_TABLE), - errmsg("relation \"%s\" is not a parent of relation \"%s\"", - RelationGetRelationName(parent_rel), - RelationGetRelationName(rel)))); + { + if (child_is_partition) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation \"%s\" is not a partition of relation \"%s\"", + RelationGetRelationName(child_rel), + RelationGetRelationName(parent_rel)))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation \"%s\" is not a parent of relation \"%s\"", + RelationGetRelationName(parent_rel), + RelationGetRelationName(child_rel)))); + } /* * Search through child columns looking for ones matching parent rel @@ -10769,7 +11154,7 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) ScanKeyInit(&key[0], Anum_pg_attribute_attrelid, BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(rel))); + ObjectIdGetDatum(RelationGetRelid(child_rel))); scan = systable_beginscan(catalogRelation, AttributeRelidNumIndexId, true, NULL, 1, key); while (HeapTupleIsValid(attributeTuple = systable_getnext(scan))) @@ -10831,7 +11216,7 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(rel))); + ObjectIdGetDatum(RelationGetRelid(child_rel))); scan = systable_beginscan(catalogRelation, ConstraintRelidIndexId, true, NULL, 1, key); @@ -10862,7 +11247,7 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) if (copy_con->coninhcount <= 0) /* shouldn't happen */ elog(ERROR, "relation %u has non-inherited constraint \"%s\"", - RelationGetRelid(rel), NameStr(copy_con->conname)); + RelationGetRelid(child_rel), NameStr(copy_con->conname)); copy_con->coninhcount--; if (copy_con->coninhcount == 0) @@ -10874,30 +11259,20 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) } } - parent_oid = RelationGetRelid(parent_rel); - systable_endscan(scan); heap_close(catalogRelation, RowExclusiveLock); - drop_parent_dependency(RelationGetRelid(rel), + drop_parent_dependency(RelationGetRelid(child_rel), RelationRelationId, RelationGetRelid(parent_rel)); - /* * Post alter hook of this inherits. Since object_access_hook doesn't take * multiple object identifiers, we relay oid of parent relation using * auxiliary_id argument. */ InvokeObjectPostAlterHookArg(InheritsRelationId, - RelationGetRelid(rel), 0, + RelationGetRelid(child_rel), 0, RelationGetRelid(parent_rel), false); - - /* keep our lock on the parent relation until commit */ - heap_close(parent_rel, NoLock); - - ObjectAddressSet(address, RelationRelationId, parent_oid); - - return address; } /* @@ -12582,3 +12957,428 @@ ComputePartitionAttrs(Relation rel, List *partParams, AttrNumber *partattrs, attn++; } } + +/* + * ALTER TABLE ATTACH PARTITION FOR VALUES + * + * Return the address of the newly attached partition. + */ +static ObjectAddress +ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd) +{ + PartitionKey key = RelationGetPartitionKey(rel); + Relation attachRel, + catalog; + List *childrels; + TupleConstr *attachRel_constr; + List *partConstraint, + *existConstraint; + SysScanDesc scan; + ScanKeyData skey; + HeapTuple tuple; + AttrNumber attno; + int natts; + TupleDesc tupleDesc; + bool skip_validate = false; + ObjectAddress address; + + attachRel = heap_openrv(cmd->name, AccessExclusiveLock); + + /* + * Must be owner of both parent and source table -- parent was checked by + * ATSimplePermissions call in ATPrepCmd + */ + ATSimplePermissions(attachRel, ATT_TABLE | ATT_FOREIGN_TABLE); + + /* A partition can only have one parent */ + if (attachRel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is already a partition", + RelationGetRelationName(attachRel)))); + + if (attachRel->rd_rel->reloftype) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach a typed table as partition"))); + + /* + * attachRel should not already be part of inheritance; either as a child + * table... + */ + catalog = heap_open(InheritsRelationId, AccessShareLock); + ScanKeyInit(&skey, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(attachRel))); + scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true, + NULL, 1, &skey); + if (HeapTupleIsValid(systable_getnext(scan))) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach inheritance child as partition"))); + systable_endscan(scan); + + /* ...or be a RELKIND_RELATION parent table */ + ScanKeyInit(&skey, + Anum_pg_inherits_inhparent, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(attachRel))); + scan = systable_beginscan(catalog, InheritsParentIndexId, true, NULL, + 1, &skey); + if (HeapTupleIsValid(systable_getnext(scan)) && + attachRel->rd_rel->relkind == RELKIND_RELATION) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach inheritance parent as partition"))); + systable_endscan(scan); + heap_close(catalog, AccessShareLock); + + /* + * Prevent circularity by seeing if rel is a partition of attachRel. + * (In particular, this disallows making a rel a partition of itself.) + */ + childrels = find_all_inheritors(RelationGetRelid(attachRel), + AccessShareLock, NULL); + if (list_member_oid(childrels, RelationGetRelid(rel))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("circular inheritance not allowed"), + errdetail("\"%s\" is already a child of \"%s\".", + RelationGetRelationName(rel), + RelationGetRelationName(attachRel)))); + + /* If attachRel is temp, it must belong to this session */ + if (attachRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && + !attachRel->rd_islocaltemp) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach a temporary relation of another session as partition"))); + + /* If parent has OIDs then child must have OIDs */ + if (rel->rd_rel->relhasoids && !attachRel->rd_rel->relhasoids) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach table \"%s\" without OIDs as partition of" + " table \"%s\" with OIDs", RelationGetRelationName(attachRel), + RelationGetRelationName(rel)))); + + /* OTOH, if parent doesn't have them, do not allow in attachRel either */ + if (attachRel->rd_rel->relhasoids && !rel->rd_rel->relhasoids) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach table \"%s\" with OIDs as partition of table" + " \"%s\" without OIDs", RelationGetRelationName(attachRel), + RelationGetRelationName(rel)))); + + /* Check if there are any columns in attachRel that aren't in the parent */ + tupleDesc = RelationGetDescr(attachRel); + natts = tupleDesc->natts; + for (attno = 1; attno <= natts; attno++) + { + Form_pg_attribute attribute = tupleDesc->attrs[attno - 1]; + char *attributeName = NameStr(attribute->attname); + + /* Ignore dropped */ + if (attribute->attisdropped) + continue; + + /* Find same column in parent (matching on column name). */ + tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), attributeName); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table \"%s\" contains column \"%s\" not found in parent \"%s\"", + RelationGetRelationName(attachRel), attributeName, + RelationGetRelationName(rel)), + errdetail("New partition should contain only the columns present in parent."))); + } + + /* OK to create inheritance. Rest of the checks performed there */ + CreateInheritance(attachRel, rel); + + /* + * Check that the new partition's bound is valid and does not overlap any + * of existing partitions of the parent - note that it does not return + * on error. + */ + check_new_partition_bound(RelationGetRelationName(attachRel), rel, + cmd->bound); + + /* Update the pg_class entry. */ + StorePartitionBound(attachRel, cmd->bound); + + /* + * Generate partition constraint from the partition bound specification. + * If the parent itself is a partition, make sure to include its + * constraint as well. + */ + partConstraint = list_concat(get_qual_from_partbound(attachRel, rel, + cmd->bound), + RelationGetPartitionQual(rel, true)); + partConstraint = (List *) eval_const_expressions(NULL, + (Node *) partConstraint); + partConstraint = (List *) canonicalize_qual((Expr *) partConstraint); + partConstraint = list_make1(make_ands_explicit(partConstraint)); + + /* + * Check if we can do away with having to scan the table being attached + * to validate the partition constraint, by *proving* that the existing + * constraints of the table *imply* the partition predicate. We include + * the table's check constraints and NOT NULL constraints in the list of + * clauses passed to predicate_implied_by(). + * + * There is a case in which we cannot rely on just the result of the + * proof. + */ + tupleDesc = RelationGetDescr(attachRel); + attachRel_constr = tupleDesc->constr; + existConstraint = NIL; + if (attachRel_constr > 0) + { + int num_check = attachRel_constr->num_check; + int i; + Bitmapset *not_null_attrs = NULL; + + if (attachRel_constr->has_not_null) + { + int natts = attachRel->rd_att->natts; + + for (i = 1; i <= natts; i++) + { + Form_pg_attribute att = attachRel->rd_att->attrs[i - 1]; + + if (att->attnotnull && !att->attisdropped) + { + NullTest *ntest = makeNode(NullTest); + + ntest->arg = (Expr *) makeVar(1, + i, + att->atttypid, + att->atttypmod, + att->attcollation, + 0); + ntest->nulltesttype = IS_NOT_NULL; + + /* + * argisrow=false is correct even for a composite column, + * because attnotnull does not represent a SQL-spec IS NOT + * NULL test in such a case, just IS DISTINCT FROM NULL. + */ + ntest->argisrow = false; + ntest->location = -1; + existConstraint = lappend(existConstraint, ntest); + not_null_attrs = bms_add_member(not_null_attrs, i); + } + } + } + + for (i = 0; i < num_check; i++) + { + Node *cexpr; + + /* + * If this constraint hasn't been fully validated yet, we must + * ignore it here. + */ + if (!attachRel_constr->check[i].ccvalid) + continue; + + cexpr = stringToNode(attachRel_constr->check[i].ccbin); + + /* + * Run each expression through const-simplification and + * canonicalization. It is necessary, because we will be + * comparing it to similarly-processed qual clauses, and may fail + * to detect valid matches without this. + */ + cexpr = eval_const_expressions(NULL, cexpr); + cexpr = (Node *) canonicalize_qual((Expr *) cexpr); + + existConstraint = list_concat(existConstraint, + make_ands_implicit((Expr *) cexpr)); + } + + existConstraint = list_make1(make_ands_explicit(existConstraint)); + + /* And away we go ... */ + if (predicate_implied_by(partConstraint, existConstraint)) + skip_validate = true; + + /* + * We choose to err on the safer side, ie, give up on skipping the + * the validation scan, if the partition key column doesn't have + * the NOT NULL constraint and the table is to become a list partition + * that does not accept nulls. In this case, the partition predicate + * (partConstraint) does include an IS NOT NULL expression, however, + * because of the way predicate_implied_by_simple_clause() is designed + * to handle IS NOT NULL predicates in the absence of a IS NOT NULL + * clause, we cannot rely on just the above proof. + * + * That is not an issue in case of a range partition, because if there + * were no NOT NULL constraint defined on the key columns, an error + * would be thrown before we get here anyway. + */ + if (key->strategy == PARTITION_STRATEGY_LIST) + { + List *part_constr; + ListCell *lc; + bool partition_accepts_null = true; + + /* + * Partition does not accept nulls if there is a IS NOT NULL + * expression in the partition constraint. + */ + part_constr = linitial(partConstraint); + part_constr = make_ands_implicit((Expr *) part_constr); + foreach(lc, part_constr) + { + Node *expr = lfirst(lc); + + if (IsA(expr, NullTest) && + ((NullTest *) expr)->nulltesttype == IS_NOT_NULL) + { + partition_accepts_null = false; + break; + } + } + + if (!partition_accepts_null && + !bms_is_member(get_partition_col_attnum(key, 0), + not_null_attrs)) + skip_validate = false; + } + } + + if (skip_validate) + elog(NOTICE, "skipping scan to validate partition constraint"); + + /* + * Set up to have the table to be scanned to validate the partition + * constraint (see partConstraint above). If it's a partitioned table, + * we instead schdule its leaf partitions to be scanned instead. + */ + if (!skip_validate) + { + List *all_parts; + ListCell *lc; + + /* Take an exclusive lock on the partitions to be checked */ + if (attachRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + all_parts = find_all_inheritors(RelationGetRelid(attachRel), + AccessExclusiveLock, NULL); + else + all_parts = list_make1_oid(RelationGetRelid(attachRel)); + + foreach(lc, all_parts) + { + AlteredTableInfo *tab; + Oid part_relid = lfirst_oid(lc); + Relation part_rel; + Expr *constr; + + /* Lock already taken */ + if (part_relid != RelationGetRelid(attachRel)) + part_rel = heap_open(part_relid, NoLock); + else + part_rel = attachRel; + + /* + * Skip if it's a partitioned table. Only RELKIND_RELATION + * relations (ie, leaf partitions) need to be scanned. + */ + if (part_rel != attachRel && + part_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + heap_close(part_rel, NoLock); + continue; + } + + /* Grab a work queue entry */ + tab = ATGetQueueEntry(wqueue, part_rel); + + constr = linitial(partConstraint); + tab->partition_constraint = make_ands_implicit((Expr *) constr); + + /* keep our lock until commit */ + if (part_rel != attachRel) + heap_close(part_rel, NoLock); + } + } + + /* + * Invalidate the relcache so that the new partition is now included + * in rel's partition descriptor. + */ + CacheInvalidateRelcache(rel); + + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(attachRel)); + + /* keep our lock until commit */ + heap_close(attachRel, NoLock); + + return address; +} + +/* + * ALTER TABLE DETACH PARTITION + * + * Return the address of the relation that is no longer a partition of rel. + */ +static ObjectAddress +ATExecDetachPartition(Relation rel, RangeVar *name) +{ + Relation partRel, + classRel; + HeapTuple tuple, + newtuple; + Datum new_val[Natts_pg_class]; + bool isnull, + new_null[Natts_pg_class], + new_repl[Natts_pg_class]; + ObjectAddress address; + + partRel = heap_openrv(name, AccessShareLock); + + /* All inheritance related checks are performed within the function */ + RemoveInheritance(partRel, rel); + + /* Update pg_class tuple */ + classRel = heap_open(RelationRelationId, RowExclusiveLock); + tuple = SearchSysCacheCopy1(RELOID, + ObjectIdGetDatum(RelationGetRelid(partRel))); + Assert(((Form_pg_class) GETSTRUCT(tuple))->relispartition); + + (void) SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relpartbound, + &isnull); + Assert(!isnull); + + /* Clear relpartbound and reset relispartition */ + memset(new_val, 0, sizeof(new_val)); + memset(new_null, false, sizeof(new_null)); + memset(new_repl, false, sizeof(new_repl)); + new_val[Anum_pg_class_relpartbound - 1] = (Datum) 0; + new_null[Anum_pg_class_relpartbound - 1] = true; + new_repl[Anum_pg_class_relpartbound - 1] = true; + newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel), + new_val, new_null, new_repl); + + ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = false; + simple_heap_update(classRel, &newtuple->t_self, newtuple); + CatalogUpdateIndexes(classRel, newtuple); + heap_freetuple(newtuple); + heap_close(classRel, RowExclusiveLock); + + /* + * Invalidate the relcache so that the partition is no longer included + * in our partition descriptor. + */ + CacheInvalidateRelcache(rel); + + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); + + /* keep our lock until commit */ + heap_close(partRel, NoLock); + + return address; +} diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 056933a..5e3989a 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -2107,7 +2107,8 @@ DefineCompositeType(RangeVar *typevar, List *coldeflist) /* * Finally create the relation. This also creates the type. */ - DefineRelation(createStmt, RELKIND_COMPOSITE_TYPE, InvalidOid, &address); + DefineRelation(createStmt, RELKIND_COMPOSITE_TYPE, InvalidOid, &address, + NULL); return address; } diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index 325a810..c6b0e4f 100644 --- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -228,7 +228,8 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace, * existing view, so we don't need more code to complain if "replace" * is false). */ - address = DefineRelation(createStmt, RELKIND_VIEW, InvalidOid, NULL); + address = DefineRelation(createStmt, RELKIND_VIEW, InvalidOid, NULL, + NULL); Assert(address.objectId != InvalidOid); return address; } diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 1c978c0..28d0036 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3031,6 +3031,7 @@ CopyCreateStmtFields(const CreateStmt *from, CreateStmt *newnode) COPY_NODE_FIELD(tableElts); COPY_NODE_FIELD(inhRelations); COPY_NODE_FIELD(partspec); + COPY_NODE_FIELD(partbound); COPY_NODE_FIELD(ofTypename); COPY_NODE_FIELD(constraints); COPY_NODE_FIELD(options); @@ -4215,6 +4216,43 @@ _copyPartitionElem(const PartitionElem *from) return newnode; } +static PartitionBoundSpec * +_copyPartitionBoundSpec(const PartitionBoundSpec *from) +{ + PartitionBoundSpec *newnode = makeNode(PartitionBoundSpec); + + COPY_SCALAR_FIELD(strategy); + COPY_NODE_FIELD(listdatums); + COPY_NODE_FIELD(lowerdatums); + COPY_NODE_FIELD(upperdatums); + COPY_LOCATION_FIELD(location); + + return newnode; +} + +static PartitionRangeDatum * +_copyPartitionRangeDatum(const PartitionRangeDatum *from) +{ + PartitionRangeDatum *newnode = makeNode(PartitionRangeDatum); + + COPY_SCALAR_FIELD(infinite); + COPY_NODE_FIELD(value); + COPY_LOCATION_FIELD(location); + + return newnode; +} + +static PartitionCmd * +_copyPartitionCmd(const PartitionCmd *from) +{ + PartitionCmd *newnode = makeNode(PartitionCmd); + + COPY_NODE_FIELD(name); + COPY_NODE_FIELD(bound); + + return newnode; +} + /* **************************************************************** * pg_list.h copy functions * **************************************************************** @@ -5138,6 +5176,15 @@ copyObject(const void *from) case T_PartitionElem: retval = _copyPartitionElem(from); break; + case T_PartitionBoundSpec: + retval = _copyPartitionBoundSpec(from); + break; + case T_PartitionRangeDatum: + retval = _copyPartitionRangeDatum(from); + break; + case T_PartitionCmd: + retval = _copyPartitionCmd(from); + break; /* * MISCELLANEOUS NODES diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 7d0391d..8fc32ca 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1169,6 +1169,7 @@ _equalCreateStmt(const CreateStmt *a, const CreateStmt *b) COMPARE_NODE_FIELD(tableElts); COMPARE_NODE_FIELD(inhRelations); COMPARE_NODE_FIELD(partspec); + COMPARE_NODE_FIELD(partbound); COMPARE_NODE_FIELD(ofTypename); COMPARE_NODE_FIELD(constraints); COMPARE_NODE_FIELD(options); @@ -2668,6 +2669,37 @@ _equalPartitionElem(const PartitionElem *a, const PartitionElem *b) return true; } +static bool +_equalPartitionBoundSpec(const PartitionBoundSpec *a, const PartitionBoundSpec *b) +{ + COMPARE_SCALAR_FIELD(strategy); + COMPARE_NODE_FIELD(listdatums); + COMPARE_NODE_FIELD(lowerdatums); + COMPARE_NODE_FIELD(upperdatums); + COMPARE_LOCATION_FIELD(location); + + return true; +} + +static bool +_equalPartitionRangeDatum(const PartitionRangeDatum *a, const PartitionRangeDatum *b) +{ + COMPARE_SCALAR_FIELD(infinite); + COMPARE_NODE_FIELD(value); + COMPARE_LOCATION_FIELD(location); + + return true; +} + +static bool +_equalPartitionCmd(const PartitionCmd *a, const PartitionCmd *b) +{ + COMPARE_NODE_FIELD(name); + COMPARE_NODE_FIELD(bound); + + return true; +} + /* * Stuff from pg_list.h */ @@ -3430,6 +3462,15 @@ equal(const void *a, const void *b) case T_PartitionElem: retval = _equalPartitionElem(a, b); break; + case T_PartitionBoundSpec: + retval = _equalPartitionBoundSpec(a, b); + break; + case T_PartitionRangeDatum: + retval = _equalPartitionRangeDatum(a, b); + break; + case T_PartitionCmd: + retval = _equalPartitionCmd(a, b); + break; default: elog(ERROR, "unrecognized node type: %d", diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 3997441..973fb15 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1552,6 +1552,12 @@ exprLocation(const Node *expr) /* just use nested expr's location */ loc = exprLocation((Node *) ((const InferenceElem *) expr)->expr); break; + case T_PartitionBoundSpec: + loc = ((const PartitionBoundSpec *) expr)->location; + break; + case T_PartitionRangeDatum: + loc = ((const PartitionRangeDatum *) expr)->location; + break; default: /* for any other node type it's just unknown... */ loc = -1; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 323daf5..0d858f5 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -2393,6 +2393,7 @@ _outCreateStmtInfo(StringInfo str, const CreateStmt *node) WRITE_NODE_FIELD(tableElts); WRITE_NODE_FIELD(inhRelations); WRITE_NODE_FIELD(partspec); + WRITE_NODE_FIELD(partbound); WRITE_NODE_FIELD(ofTypename); WRITE_NODE_FIELD(constraints); WRITE_NODE_FIELD(options); @@ -3300,6 +3301,26 @@ _outPartitionElem(StringInfo str, const PartitionElem *node) WRITE_LOCATION_FIELD(location); } +static void +_outPartitionBoundSpec(StringInfo str, const PartitionBoundSpec *node) +{ + WRITE_NODE_TYPE("PARTITIONBOUND"); + + WRITE_CHAR_FIELD(strategy); + WRITE_NODE_FIELD(listdatums); + WRITE_NODE_FIELD(lowerdatums); + WRITE_NODE_FIELD(upperdatums); +} + +static void +_outPartitionRangeDatum(StringInfo str, const PartitionRangeDatum *node) +{ + WRITE_NODE_TYPE("PARTRANGEDATUM"); + + WRITE_BOOL_FIELD(infinite); + WRITE_NODE_FIELD(value); +} + /* * outNode - * converts a Node into ascii string and append it to 'str' @@ -3893,6 +3914,12 @@ outNode(StringInfo str, const void *obj) case T_PartitionElem: _outPartitionElem(str, obj); break; + case T_PartitionBoundSpec: + _outPartitionBoundSpec(str, obj); + break; + case T_PartitionRangeDatum: + _outPartitionRangeDatum(str, obj); + break; default: diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 917e6c8..c587d4e 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -2266,6 +2266,36 @@ _readExtensibleNode(void) } /* + * _readPartitionBoundSpec + */ +static PartitionBoundSpec * +_readPartitionBoundSpec(void) +{ + READ_LOCALS(PartitionBoundSpec); + + READ_CHAR_FIELD(strategy); + READ_NODE_FIELD(listdatums); + READ_NODE_FIELD(lowerdatums); + READ_NODE_FIELD(upperdatums); + + READ_DONE(); +} + +/* + * _readPartitionRangeDatum + */ +static PartitionRangeDatum * +_readPartitionRangeDatum(void) +{ + READ_LOCALS(PartitionRangeDatum); + + READ_BOOL_FIELD(infinite); + READ_NODE_FIELD(value); + + READ_DONE(); +} + +/* * parseNodeString * * Given a character string representing a node tree, parseNodeString creates @@ -2497,6 +2527,10 @@ parseNodeString(void) return_value = _readAlternativeSubPlan(); else if (MATCH("EXTENSIBLENODE", 14)) return_value = _readExtensibleNode(); + else if (MATCH("PARTITIONBOUND", 14)) + return_value = _readPartitionBoundSpec(); + else if (MATCH("PARTRANGEDATUM", 14)) + return_value = _readPartitionRangeDatum(); else { elog(ERROR, "badly formatted node string \"%.32s\"...", token); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 1680fea..452d8a9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -231,6 +231,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); VariableSetStmt *vsetstmt; PartitionElem *partelem; PartitionSpec *partspec; + PartitionRangeDatum *partrange_datum; } %type stmt schema_stmt @@ -550,6 +551,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type part_strategy %type part_elem %type part_params +%type OptPartitionElementList PartitionElementList +%type PartitionElement +%type ForValues +%type partbound_datum +%type partbound_datum_list +%type PartitionRangeDatum +%type range_datum_list /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -575,7 +583,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); /* ordinary key words in alphabetical order */ %token ABORT_P ABSOLUTE_P ACCESS ACTION ADD_P ADMIN AFTER AGGREGATE ALL ALSO ALTER ALWAYS ANALYSE ANALYZE AND ANY ARRAY AS ASC - ASSERTION ASSIGNMENT ASYMMETRIC AT ATTRIBUTE AUTHORIZATION + ASSERTION ASSIGNMENT ASYMMETRIC AT ATTACH ATTRIBUTE AUTHORIZATION BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT BOOLEAN_P BOTH BY @@ -591,7 +599,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DATA_P DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DESC - DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP + DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P + DOUBLE_P DROP EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN @@ -2377,6 +2386,31 @@ alter_table_cmd: n->def = (Node *)$1; $$ = (Node *) n; } + /* ALTER TABLE ATTACH PARTITION FOR VALUES */ + | ATTACH PARTITION qualified_name ForValues + { + AlterTableCmd *n = makeNode(AlterTableCmd); + PartitionCmd *cmd = makeNode(PartitionCmd); + + n->subtype = AT_AttachPartition; + cmd->name = $3; + cmd->bound = (Node *) $4; + n->def = (Node *) cmd; + + $$ = (Node *) n; + } + /* ALTER TABLE DETACH PARTITION */ + | DETACH PARTITION qualified_name + { + AlterTableCmd *n = makeNode(AlterTableCmd); + PartitionCmd *cmd = makeNode(PartitionCmd); + + n->subtype = AT_DetachPartition; + cmd->name = $3; + n->def = (Node *) cmd; + + $$ = (Node *) n; + } ; alter_column_default: @@ -2472,6 +2506,73 @@ reloption_elem: } ; +ForValues: + /* a LIST partition */ + FOR VALUES IN_P '(' partbound_datum_list ')' + { + PartitionBoundSpec *n = makeNode(PartitionBoundSpec); + + n->strategy = PARTITION_STRATEGY_LIST; + n->listdatums = $5; + n->location = @3; + + $$ = (Node *) n; + } + + /* a RANGE partition */ + | FOR VALUES FROM '(' range_datum_list ')' TO '(' range_datum_list ')' + { + PartitionBoundSpec *n = makeNode(PartitionBoundSpec); + + n->strategy = PARTITION_STRATEGY_RANGE; + n->lowerdatums = $5; + n->upperdatums = $9; + n->location = @3; + + $$ = (Node *) n; + } + ; + +partbound_datum: + Sconst { $$ = makeStringConst($1, @1); } + | NumericOnly { $$ = makeAConst($1, @1); } + | NULL_P { $$ = makeNullAConst(@1); } + ; + +partbound_datum_list: + partbound_datum { $$ = list_make1($1); } + | partbound_datum_list ',' partbound_datum + { $$ = lappend($1, $3); } + ; + +range_datum_list: + PartitionRangeDatum { $$ = list_make1($1); } + | range_datum_list ',' PartitionRangeDatum + { $$ = lappend($1, $3); } + ; + +PartitionRangeDatum: + UNBOUNDED + { + PartitionRangeDatum *n = makeNode(PartitionRangeDatum); + + n->infinite = true; + n->value = NULL; + n->location = @1; + + $$ = n; + } + | partbound_datum + { + PartitionRangeDatum *n = makeNode(PartitionRangeDatum); + + n->infinite = false; + n->value = $1; + n->location = @1; + + $$ = n; + } + ; /***************************************************************************** * @@ -2889,6 +2990,44 @@ CreateStmt: CREATE OptTemp TABLE qualified_name '(' OptTableElementList ')' n->if_not_exists = true; $$ = (Node *)n; } + | CREATE OptTemp TABLE qualified_name PARTITION OF qualified_name + OptPartitionElementList ForValues OptPartitionSpec OptWith + OnCommitOption OptTableSpace + { + CreateStmt *n = makeNode(CreateStmt); + $4->relpersistence = $2; + n->relation = $4; + n->tableElts = $8; + n->inhRelations = list_make1($7); + n->partbound = (Node *) $9; + n->partspec = $10; + n->ofTypename = NULL; + n->constraints = NIL; + n->options = $11; + n->oncommit = $12; + n->tablespacename = $13; + n->if_not_exists = false; + $$ = (Node *)n; + } + | CREATE OptTemp TABLE IF_P NOT EXISTS qualified_name PARTITION OF + qualified_name OptPartitionElementList ForValues OptPartitionSpec + OptWith OnCommitOption OptTableSpace + { + CreateStmt *n = makeNode(CreateStmt); + $7->relpersistence = $2; + n->relation = $7; + n->tableElts = $11; + n->inhRelations = list_make1($10); + n->partbound = (Node *) $12; + n->partspec = $13; + n->ofTypename = NULL; + n->constraints = NIL; + n->options = $14; + n->oncommit = $15; + n->tablespacename = $16; + n->if_not_exists = true; + $$ = (Node *)n; + } ; /* @@ -2934,6 +3073,11 @@ OptTypedTableElementList: | /*EMPTY*/ { $$ = NIL; } ; +OptPartitionElementList: + '(' PartitionElementList ')' { $$ = $2; } + | /*EMPTY*/ { $$ = NIL; } + ; + TableElementList: TableElement { @@ -2956,6 +3100,17 @@ TypedTableElementList: } ; +PartitionElementList: + PartitionElement + { + $$ = list_make1($1); + } + | PartitionElementList ',' PartitionElement + { + $$ = lappend($1, $3); + } + ; + TableElement: columnDef { $$ = $1; } | TableLikeClause { $$ = $1; } @@ -2967,6 +3122,11 @@ TypedTableElement: | TableConstraint { $$ = $1; } ; +PartitionElement: + columnOptions { $$ = $1; } + | TableConstraint { $$ = $1; } + ; + columnDef: ColId Typename create_generic_options ColQualList { ColumnDef *n = makeNode(ColumnDef); @@ -4554,6 +4714,48 @@ CreateForeignTableStmt: n->options = $14; $$ = (Node *) n; } + | CREATE FOREIGN TABLE qualified_name + PARTITION OF qualified_name OptPartitionElementList ForValues + SERVER name create_generic_options + { + CreateForeignTableStmt *n = makeNode(CreateForeignTableStmt); + $4->relpersistence = RELPERSISTENCE_PERMANENT; + n->base.relation = $4; + n->base.inhRelations = list_make1($7); + n->base.tableElts = $8; + n->base.partbound = (Node *) $9; + n->base.ofTypename = NULL; + n->base.constraints = NIL; + n->base.options = NIL; + n->base.oncommit = ONCOMMIT_NOOP; + n->base.tablespacename = NULL; + n->base.if_not_exists = false; + /* FDW-specific data */ + n->servername = $11; + n->options = $12; + $$ = (Node *) n; + } + | CREATE FOREIGN TABLE IF_P NOT EXISTS qualified_name + PARTITION OF qualified_name OptPartitionElementList ForValues + SERVER name create_generic_options + { + CreateForeignTableStmt *n = makeNode(CreateForeignTableStmt); + $7->relpersistence = RELPERSISTENCE_PERMANENT; + n->base.relation = $7; + n->base.inhRelations = list_make1($10); + n->base.tableElts = $11; + n->base.partbound = (Node *) $12; + n->base.ofTypename = NULL; + n->base.constraints = NIL; + n->base.options = NIL; + n->base.oncommit = ONCOMMIT_NOOP; + n->base.tablespacename = NULL; + n->base.if_not_exists = true; + /* FDW-specific data */ + n->servername = $14; + n->options = $15; + $$ = (Node *) n; + } ; /***************************************************************************** @@ -13741,6 +13943,7 @@ unreserved_keyword: | ASSERTION | ASSIGNMENT | AT + | ATTACH | ATTRIBUTE | BACKWARD | BEFORE @@ -13787,6 +13990,7 @@ unreserved_keyword: | DELIMITER | DELIMITERS | DEPENDS + | DETACH | DICTIONARY | DISABLE_P | DISCARD diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index fc896a2..e64f4a8 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -47,8 +47,10 @@ #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "optimizer/planner.h" #include "parser/analyze.h" #include "parser/parse_clause.h" +#include "parser/parse_coerce.h" #include "parser/parse_collate.h" #include "parser/parse_expr.h" #include "parser/parse_relation.h" @@ -62,6 +64,7 @@ #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/ruleutils.h" #include "utils/syscache.h" #include "utils/typcache.h" @@ -88,6 +91,7 @@ typedef struct * the table */ IndexStmt *pkey; /* PRIMARY KEY index, if any */ bool ispartitioned; /* true if table is partitioned */ + Node *partbound; /* transformed FOR VALUES */ } CreateStmtContext; /* State shared by transformCreateSchemaStmt and its subroutines */ @@ -130,6 +134,7 @@ static void transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList); static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column); static void setSchemaName(char *context_schema, char **stmt_schema_name); +static void transformAttachPartition(CreateStmtContext *cxt, PartitionCmd *cmd); /* @@ -253,7 +258,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) { int partnatts = list_length(stmt->partspec->partParams); - if (stmt->inhRelations) + if (stmt->inhRelations && !stmt->partbound) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("cannot create partitioned table as inheritance child"))); @@ -2581,6 +2586,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, cxt.alist = NIL; cxt.pkey = NULL; cxt.ispartitioned = (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + cxt.partbound = NULL; /* * The only subtypes that currently require parse transformation handling @@ -2663,6 +2669,19 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, break; } + case AT_AttachPartition: + { + PartitionCmd *partcmd = (PartitionCmd *) cmd->def; + + transformAttachPartition(&cxt, partcmd); + + /* assign transformed values */ + partcmd->bound = cxt.partbound; + } + + newcmds = lappend(newcmds, cmd); + break; + default: newcmds = lappend(newcmds, cmd); break; @@ -3027,3 +3046,242 @@ setSchemaName(char *context_schema, char **stmt_schema_name) "different from the one being created (%s)", *stmt_schema_name, context_schema))); } + +/* + * transformAttachPartition + * Analyze ATTACH PARTITION ... FOR VALUES ... + */ +static void +transformAttachPartition(CreateStmtContext *cxt, PartitionCmd *cmd) +{ + Relation parentRel = cxt->rel; + + /* + * We are going to try to validate the partition bound specification + * against the partition key of rel, so it better have one. + */ + if (parentRel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("\"%s\" is not partitioned", + RelationGetRelationName(parentRel)))); + + /* tranform the values */ + Assert(RelationGetPartitionKey(parentRel) != NULL); + cxt->partbound = transformPartitionBound(cxt->pstate, parentRel, + cmd->bound); +} + +/* + * transformPartitionBound + * + * Transform partition bound specification + */ +Node * +transformPartitionBound(ParseState *pstate, Relation parent, Node *bound) +{ + PartitionBoundSpec *spec = (PartitionBoundSpec *) bound, + *result_spec; + PartitionKey key = RelationGetPartitionKey(parent); + char strategy = get_partition_strategy(key); + int partnatts = get_partition_natts(key); + List *partexprs = get_partition_exprs(key); + + result_spec = copyObject(spec); + switch (strategy) + { + case PARTITION_STRATEGY_LIST: + { + ListCell *cell; + char *colname; + + /* Get the only column's name in case we need to output an error */ + if (key->partattrs[0] != 0) + colname = get_relid_attribute_name(RelationGetRelid(parent), + key->partattrs[0]); + else + colname = deparse_expression((Node *) linitial(partexprs), + deparse_context_for(RelationGetRelationName(parent), + RelationGetRelid(parent)), + false, false); + + if (spec->strategy != PARTITION_STRATEGY_LIST) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("invalid bound specification for a list partition"), + parser_errposition(pstate, exprLocation(bound)))); + + result_spec->listdatums = NIL; + foreach(cell, spec->listdatums) + { + A_Const *con = (A_Const *) lfirst(cell); + Node *value; + ListCell *cell2; + bool duplicate; + + value = (Node *) make_const(pstate, &con->val, con->location); + value = coerce_to_target_type(pstate, + value, exprType(value), + get_partition_col_typid(key, 0), + get_partition_col_typmod(key, 0), + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + -1); + + if (value == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("specified value cannot be cast to type \"%s\" of column \"%s\"", + format_type_be(get_partition_col_typid(key, 0)), + colname), + parser_errposition(pstate, + exprLocation((Node *) con)))); + + /* Simplify the expression */ + value = (Node *) expression_planner((Expr *) value); + + /* Don't add to the result if the value is a duplicate */ + duplicate = false; + foreach(cell2, result_spec->listdatums) + { + Const *value2 = (Const *) lfirst(cell2); + + if (equal(value, value2)) + { + duplicate = true; + break; + } + } + if (duplicate) + continue; + + result_spec->listdatums = lappend(result_spec->listdatums, + value); + } + + break; + } + + case PARTITION_STRATEGY_RANGE: + { + ListCell *cell1, + *cell2; + int i, + j; + char *colname; + + if (spec->strategy != PARTITION_STRATEGY_RANGE) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("invalid bound specification for a range partition"), + parser_errposition(pstate, exprLocation(bound)))); + + Assert(spec->lowerdatums != NIL && spec->upperdatums != NIL); + + if (list_length(spec->lowerdatums) != partnatts) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("FROM must specify exactly one value per partitioning column"))); + if (list_length(spec->upperdatums) != partnatts) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("TO must specify exactly one value per partitioning column"))); + + i = j = 0; + result_spec->lowerdatums = result_spec->upperdatums = NIL; + forboth (cell1, spec->lowerdatums, cell2, spec->upperdatums) + { + PartitionRangeDatum *ldatum, + *rdatum; + Node *value; + A_Const *lcon = NULL, + *rcon = NULL; + + ldatum = (PartitionRangeDatum *) lfirst(cell1); + rdatum = (PartitionRangeDatum *) lfirst(cell2); + /* Get the column's name in case we need to output an error */ + if (key->partattrs[i] != 0) + colname = get_relid_attribute_name(RelationGetRelid(parent), + key->partattrs[i]); + else + { + colname = deparse_expression((Node *) list_nth(partexprs, j), + deparse_context_for(RelationGetRelationName(parent), + RelationGetRelid(parent)), + false, false); + ++j; + } + + if (!ldatum->infinite) + lcon = (A_Const *) ldatum->value; + if (!rdatum->infinite) + rcon = (A_Const *) rdatum->value; + + if (lcon) + { + value = (Node *) make_const(pstate, &lcon->val, lcon->location); + if (((Const *) value)->constisnull) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot specify NULL in range bound"))); + value = coerce_to_target_type(pstate, + value, exprType(value), + get_partition_col_typid(key, i), + get_partition_col_typmod(key, i), + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + -1); + if (value == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("specified value cannot be cast to type \"%s\" of column \"%s\"", + format_type_be(get_partition_col_typid(key, i)), + colname), + parser_errposition(pstate, exprLocation((Node *) ldatum)))); + + /* Simplify the expression */ + value = (Node *) expression_planner((Expr *) value); + ldatum->value = value; + } + + if (rcon) + { + value = (Node *) make_const(pstate, &rcon->val, rcon->location); + if (((Const *) value)->constisnull) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot specify NULL in range bound"))); + value = coerce_to_target_type(pstate, + value, exprType(value), + get_partition_col_typid(key, i), + get_partition_col_typmod(key, i), + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + -1); + if (value == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("specified value cannot be cast to type \"%s\" of column \"%s\"", + format_type_be(get_partition_col_typid(key, i)), + colname), + parser_errposition(pstate, exprLocation((Node *) rdatum)))); + + /* Simplify the expression */ + value = (Node *) expression_planner((Expr *) value); + rdatum->value = value; + } + + result_spec->lowerdatums = lappend(result_spec->lowerdatums, + copyObject(ldatum)); + result_spec->upperdatums = lappend(result_spec->upperdatums, + copyObject(rdatum)); + + ++i; + } + + break; + } + } + + return (Node *) result_spec; +} diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index f50ce40..fd4eff4 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -987,7 +987,8 @@ ProcessUtilitySlow(ParseState *pstate, /* Create the table itself */ address = DefineRelation((CreateStmt *) stmt, RELKIND_RELATION, - InvalidOid, NULL); + InvalidOid, NULL, + queryString); EventTriggerCollectSimpleCommand(address, secondaryObject, stmt); @@ -1020,7 +1021,8 @@ ProcessUtilitySlow(ParseState *pstate, /* Create the table itself */ address = DefineRelation((CreateStmt *) stmt, RELKIND_FOREIGN_TABLE, - InvalidOid, NULL); + InvalidOid, NULL, + queryString); CreateForeignTable((CreateForeignTableStmt *) stmt, address.objectId); EventTriggerCollectSimpleCommand(address, diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 7f3ba74..c958092 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -41,6 +41,7 @@ #include "catalog/index.h" #include "catalog/indexing.h" #include "catalog/namespace.h" +#include "catalog/partition.h" #include "catalog/pg_am.h" #include "catalog/pg_amproc.h" #include "catalog/pg_attrdef.h" @@ -282,6 +283,8 @@ static OpClassCacheEnt *LookupOpclassInfo(Oid operatorClassOid, StrategyNumber numSupport); static void RelationCacheInitFileRemoveInDir(const char *tblspcpath); static void unlink_initfile(const char *initfilename); +static bool equalPartitionDescs(PartitionKey key, PartitionDesc partdesc1, + PartitionDesc partdesc2); /* @@ -1158,6 +1161,58 @@ equalRSDesc(RowSecurityDesc *rsdesc1, RowSecurityDesc *rsdesc2) } /* + * equalPartitionDescs + * Compare two partition descriptors for logical equality + */ +static bool +equalPartitionDescs(PartitionKey key, PartitionDesc partdesc1, + PartitionDesc partdesc2) +{ + int i; + + if (partdesc1 != NULL) + { + if (partdesc2 == NULL) + return false; + if (partdesc1->nparts != partdesc2->nparts) + return false; + + Assert(key != NULL || partdesc1->nparts == 0); + + /* + * Same oids? If the partitioning structure did not change, that is, + * no partitions were added or removed to the relation, the oids array + * should still match element-by-element. + */ + for (i = 0; i < partdesc1->nparts; i++) + { + if (partdesc1->oids[i] != partdesc2->oids[i]) + return false; + } + + /* + * Now compare partition bound collections. The logic to iterate over + * the collections is local to partition.c. + */ + if (partdesc1->boundinfo != NULL) + { + if (partdesc2->boundinfo == NULL) + return false; + + if (!partition_bounds_equal(key, partdesc1->boundinfo, + partdesc2->boundinfo)) + return false; + } + else if (partdesc2->boundinfo != NULL) + return false; + } + else if (partdesc2 != NULL) + return false; + + return true; +} + +/* * RelationBuildDesc * * Build a relation descriptor. The caller must hold at least @@ -1285,13 +1340,18 @@ RelationBuildDesc(Oid targetRelId, bool insertIt) relation->rd_fkeylist = NIL; relation->rd_fkeyvalid = false; - /* if it's a partitioned table, initialize key info */ + /* if a partitioned table, initialize key and partition descriptor info */ if (relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { RelationBuildPartitionKey(relation); + RelationBuildPartitionDesc(relation); + } else { relation->rd_partkeycxt = NULL; relation->rd_partkey = NULL; + relation->rd_partdesc = NULL; + relation->rd_pdcxt = NULL; } /* @@ -2288,6 +2348,10 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc) MemoryContextDelete(relation->rd_rsdesc->rscxt); if (relation->rd_partkeycxt) MemoryContextDelete(relation->rd_partkeycxt); + if (relation->rd_pdcxt) + MemoryContextDelete(relation->rd_pdcxt); + if (relation->rd_partcheck) + pfree(relation->rd_partcheck); if (relation->rd_fdwroutine) pfree(relation->rd_fdwroutine); pfree(relation); @@ -2436,11 +2500,12 @@ RelationClearRelation(Relation relation, bool rebuild) * * When rebuilding an open relcache entry, we must preserve ref count, * rd_createSubid/rd_newRelfilenodeSubid, and rd_toastoid state. Also - * attempt to preserve the pg_class entry (rd_rel), tupledesc, and - * rewrite-rule substructures in place, because various places assume - * that these structures won't move while they are working with an - * open relcache entry. (Note: the refcount mechanism for tupledescs - * might someday allow us to remove this hack for the tupledesc.) + * attempt to preserve the pg_class entry (rd_rel), tupledesc, + * rewrite-rule, and partition descriptor substructures in place, + * because various places assume that these structures won't move while + * they are working with an open relcache entry. (Note: the refcount + * mechanism for tupledescs might someday allow us to remove this hack + * for the tupledesc.) * * Note that this process does not touch CurrentResourceOwner; which * is good because whatever ref counts the entry may have do not @@ -2451,6 +2516,7 @@ RelationClearRelation(Relation relation, bool rebuild) bool keep_tupdesc; bool keep_rules; bool keep_policies; + bool keep_partdesc; /* Build temporary entry, but don't link it into hashtable */ newrel = RelationBuildDesc(save_relid, false); @@ -2481,6 +2547,9 @@ RelationClearRelation(Relation relation, bool rebuild) keep_tupdesc = equalTupleDescs(relation->rd_att, newrel->rd_att); keep_rules = equalRuleLocks(relation->rd_rules, newrel->rd_rules); keep_policies = equalRSDesc(relation->rd_rsdesc, newrel->rd_rsdesc); + keep_partdesc = equalPartitionDescs(relation->rd_partkey, + relation->rd_partdesc, + newrel->rd_partdesc); /* * Perform swapping of the relcache entry contents. Within this @@ -2536,6 +2605,13 @@ RelationClearRelation(Relation relation, bool rebuild) /* pgstat_info must be preserved */ SWAPFIELD(struct PgStat_TableStatus *, pgstat_info); + /* preserve old partdesc if no logical change */ + if (keep_partdesc) + { + SWAPFIELD(PartitionDesc, rd_partdesc); + SWAPFIELD(MemoryContext, rd_pdcxt); + } + #undef SWAPFIELD /* And now we can throw away the temporary entry */ @@ -3770,6 +3846,9 @@ RelationCacheInitializePhase3(void) RelationBuildPartitionKey(relation); Assert(relation->rd_partkey != NULL); + RelationBuildPartitionDesc(relation); + Assert(relation->rd_partdesc != NULL); + restart = true; } @@ -5298,6 +5377,8 @@ load_relcache_init_file(bool shared) rel->rd_rsdesc = NULL; rel->rd_partkeycxt = NULL; rel->rd_partkey = NULL; + rel->rd_partdesc = NULL; + rel->rd_partcheck = NIL; rel->rd_indexprs = NIL; rel->rd_indpred = NIL; rel->rd_exclops = NULL; diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index 11b16a9..77dc198 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -143,5 +143,6 @@ extern void StorePartitionKey(Relation rel, Oid *partopclass, Oid *partcollation); extern void RemovePartitionKeyByRelId(Oid relid); +extern void StorePartitionBound(Relation rel, Node *bound); #endif /* HEAP_H */ diff --git a/src/include/catalog/partition.h b/src/include/catalog/partition.h new file mode 100644 index 0000000..70d8325 --- /dev/null +++ b/src/include/catalog/partition.h @@ -0,0 +1,48 @@ +/*------------------------------------------------------------------------- + * + * partition.h + * Header file for structures and utility functions related to + * partitioning + * + * Copyright (c) 2007-2016, PostgreSQL Global Development Group + * + * src/include/catalog/partition.h + * + *------------------------------------------------------------------------- + */ +#ifndef PARTITION_H +#define PARTITION_H + +#include "fmgr.h" +#include "parser/parse_node.h" +#include "utils/rel.h" + +/* + * PartitionBoundInfo encapsulates a set of partition bounds. It is usually + * associated with partitioned tables as part of its partition descriptor. + * + * The internal structure is opaque outside partition.c. + */ +typedef struct PartitionBoundInfoData *PartitionBoundInfo; + +/* + * Information about partitions of a partitioned table. + */ +typedef struct PartitionDescData +{ + int nparts; /* Number of partitions */ + Oid *oids; /* OIDs of partitions */ + PartitionBoundInfo boundinfo; /* collection of partition bounds */ +} PartitionDescData; + +typedef struct PartitionDescData *PartitionDesc; + +extern void RelationBuildPartitionDesc(Relation relation); +extern bool partition_bounds_equal(PartitionKey key, + PartitionBoundInfo p1, PartitionBoundInfo p2); + +extern void check_new_partition_bound(char *relname, Relation parent, Node *bound); +extern Oid get_partition_parent(Oid relid); +extern List *get_qual_from_partbound(Relation rel, Relation parent, Node *bound); +extern List *RelationGetPartitionQual(Relation rel, bool recurse); +#endif /* PARTITION_H */ diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h index 6a86c93..a61b7a2 100644 --- a/src/include/catalog/pg_class.h +++ b/src/include/catalog/pg_class.h @@ -70,6 +70,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO * not */ bool relispopulated; /* matview currently holds query results */ char relreplident; /* see REPLICA_IDENTITY_xxx constants */ + bool relispartition; /* is relation a partition? */ TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */ TransactionId relminmxid; /* all multixacts in this rel are >= this. * this is really a MultiXactId */ @@ -78,6 +79,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO /* NOTE: These fields are not present in a relcache entry's rd_rel field. */ aclitem relacl[1]; /* access permissions */ text reloptions[1]; /* access-method-specific options */ + pg_node_tree relpartbound; /* partition bound node tree */ #endif } FormData_pg_class; @@ -97,7 +99,7 @@ typedef FormData_pg_class *Form_pg_class; * ---------------- */ -#define Natts_pg_class 31 +#define Natts_pg_class 33 #define Anum_pg_class_relname 1 #define Anum_pg_class_relnamespace 2 #define Anum_pg_class_reltype 3 @@ -125,10 +127,12 @@ typedef FormData_pg_class *Form_pg_class; #define Anum_pg_class_relforcerowsecurity 25 #define Anum_pg_class_relispopulated 26 #define Anum_pg_class_relreplident 27 -#define Anum_pg_class_relfrozenxid 28 -#define Anum_pg_class_relminmxid 29 -#define Anum_pg_class_relacl 30 -#define Anum_pg_class_reloptions 31 +#define Anum_pg_class_relispartition 28 +#define Anum_pg_class_relfrozenxid 29 +#define Anum_pg_class_relminmxid 30 +#define Anum_pg_class_relacl 31 +#define Anum_pg_class_reloptions 32 +#define Anum_pg_class_relpartbound 33 /* ---------------- * initial contents of pg_class @@ -143,13 +147,13 @@ typedef FormData_pg_class *Form_pg_class; * Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId; * similarly, "1" in relminmxid stands for FirstMultiXactId */ -DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ )); +DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n f 3 1 _null_ _null_ _null_)); DESCR(""); -DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ )); +DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n f 3 1 _null_ _null_ _null_)); DESCR(""); -DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ )); +DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n f 3 1 _null_ _null_ _null_)); DESCR(""); -DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ )); +DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 33 0 t f f f f f f t n f 3 1 _null_ _null_ _null_)); DESCR(""); diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index 7a770f4..fa48f2e 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -23,7 +23,7 @@ extern ObjectAddress DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, - ObjectAddress *typaddress); + ObjectAddress *typaddress, const char *queryString); extern void RemoveRelations(DropStmt *drop); diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index b27412c..c514d3f 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -406,6 +406,7 @@ typedef enum NodeTag T_AlterPolicyStmt, T_CreateTransformStmt, T_CreateAmStmt, + T_PartitionCmd, /* * TAGS FOR PARSE TREE NODES (parsenodes.h) @@ -456,6 +457,8 @@ typedef enum NodeTag T_TriggerTransition, T_PartitionElem, T_PartitionSpec, + T_PartitionBoundSpec, + T_PartitionRangeDatum, /* * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h) diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d30c82b..427eff2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -728,6 +728,51 @@ typedef struct PartitionSpec #define PARTITION_STRATEGY_LIST 'l' #define PARTITION_STRATEGY_RANGE 'r' +/* + * PartitionBoundSpec - a partition bound specification + */ +typedef struct PartitionBoundSpec +{ + NodeTag type; + + char strategy; + + /* List partition values */ + List *listdatums; + + /* + * Range partition lower and upper bounds; each member of the lists + * is a PartitionRangeDatum (see below). + */ + List *lowerdatums; + List *upperdatums; + + int location; +} PartitionBoundSpec; + +/* + * PartitionRangeDatum + */ +typedef struct PartitionRangeDatum +{ + NodeTag type; + + bool infinite; + Node *value; + + int location; +} PartitionRangeDatum; + +/* + * PartitionCmd - ALTER TABLE partition commands + */ +typedef struct PartitionCmd +{ + NodeTag type; + RangeVar *name; + Node *bound; +} PartitionCmd; + /**************************************************************************** * Nodes for a Query tree ****************************************************************************/ @@ -1577,7 +1622,9 @@ typedef enum AlterTableType AT_DisableRowSecurity, /* DISABLE ROW SECURITY */ AT_ForceRowSecurity, /* FORCE ROW SECURITY */ AT_NoForceRowSecurity, /* NO FORCE ROW SECURITY */ - AT_GenericOptions /* OPTIONS (...) */ + AT_GenericOptions, /* OPTIONS (...) */ + AT_AttachPartition, /* ATTACH PARTITION */ + AT_DetachPartition /* DETACH PARTITION */ } AlterTableType; typedef struct ReplicaIdentityStmt @@ -1803,7 +1850,8 @@ typedef struct CreateStmt List *tableElts; /* column definitions (list of ColumnDef) */ List *inhRelations; /* relations to inherit from (list of * inhRelation) */ - PartitionSpec *partspec; /* PARTITION BY clause */ + Node *partbound; /* FOR VALUES clause */ + PartitionSpec *partspec; /* PARTITION BY clause */ TypeName *ofTypename; /* OF typename */ List *constraints; /* constraints (list of Constraint nodes) */ List *options; /* options from WITH clause */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 77d873b..581ff6e 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -49,6 +49,7 @@ PG_KEYWORD("assertion", ASSERTION, UNRESERVED_KEYWORD) PG_KEYWORD("assignment", ASSIGNMENT, UNRESERVED_KEYWORD) PG_KEYWORD("asymmetric", ASYMMETRIC, RESERVED_KEYWORD) PG_KEYWORD("at", AT, UNRESERVED_KEYWORD) +PG_KEYWORD("attach", ATTACH, UNRESERVED_KEYWORD) PG_KEYWORD("attribute", ATTRIBUTE, UNRESERVED_KEYWORD) PG_KEYWORD("authorization", AUTHORIZATION, TYPE_FUNC_NAME_KEYWORD) PG_KEYWORD("backward", BACKWARD, UNRESERVED_KEYWORD) @@ -127,6 +128,7 @@ PG_KEYWORD("delimiter", DELIMITER, UNRESERVED_KEYWORD) PG_KEYWORD("delimiters", DELIMITERS, UNRESERVED_KEYWORD) PG_KEYWORD("depends", DEPENDS, UNRESERVED_KEYWORD) PG_KEYWORD("desc", DESC, RESERVED_KEYWORD) +PG_KEYWORD("detach", DETACH, UNRESERVED_KEYWORD) PG_KEYWORD("dictionary", DICTIONARY, UNRESERVED_KEYWORD) PG_KEYWORD("disable", DISABLE_P, UNRESERVED_KEYWORD) PG_KEYWORD("discard", DISCARD, UNRESERVED_KEYWORD) diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h index be3b6f7..783bb00 100644 --- a/src/include/parser/parse_utilcmd.h +++ b/src/include/parser/parse_utilcmd.h @@ -25,5 +25,7 @@ extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt, extern void transformRuleStmt(RuleStmt *stmt, const char *queryString, List **actions, Node **whereClause); extern List *transformCreateSchemaStmt(CreateSchemaStmt *stmt); +extern Node *transformPartitionBound(ParseState *pstate, Relation parent, + Node *bound); #endif /* PARSE_UTILCMD_H */ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 60d8de3..cd7ea1d 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -125,6 +125,9 @@ typedef struct RelationData MemoryContext rd_partkeycxt; /* private memory cxt for the below */ struct PartitionKeyData *rd_partkey; /* partition key, or NULL */ + MemoryContext rd_pdcxt; /* private context for partdesc */ + struct PartitionDescData *rd_partdesc; /* partitions, or NULL */ + List *rd_partcheck; /* partition CHECK quals */ /* data managed by RelationGetIndexList: */ List *rd_indexlist; /* list of OIDs of indexes on relation */ @@ -602,6 +605,24 @@ get_partition_col_attnum(PartitionKey key, int col) return key->partattrs[col]; } +static inline Oid +get_partition_col_typid(PartitionKey key, int col) +{ + return key->parttypid[col]; +} + +static inline int32 +get_partition_col_typmod(PartitionKey key, int col) +{ + return key->parttypmod[col]; +} + +/* + * RelationGetPartitionDesc + * Returns partition descriptor for a relation. + */ +#define RelationGetPartitionDesc(relation) ((relation)->rd_partdesc) + /* routines in utils/cache/relcache.c */ extern void RelationIncrementReferenceCount(Relation rel); extern void RelationDecrementReferenceCount(Relation rel); diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index df6fe13..1ed2558 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -3020,3 +3020,294 @@ ERROR: cannot inherit from partitioned table "partitioned" ALTER TABLE partitioned ADD CONSTRAINT chk_a CHECK (a > 0) NO INHERIT; ERROR: cannot add NO INHERIT constraint to partitioned table "partitioned" DROP TABLE partitioned, foo; +-- +-- ATTACH PARTITION +-- +-- check that target table is partitioned +CREATE TABLE unparted ( + a int +); +CREATE TABLE fail_part (like unparted); +ALTER TABLE unparted ATTACH PARTITION fail_part FOR VALUES IN ('a'); +ERROR: "unparted" is not partitioned +DROP TABLE unparted, fail_part; +-- check that partition bound is compatible +CREATE TABLE list_parted ( + a int NOT NULL, + b char(2) COLLATE "en_US", + CONSTRAINT check_a CHECK (a > 0) +) PARTITION BY LIST (a); +CREATE TABLE fail_part (LIKE list_parted); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES FROM (1) TO (10); +ERROR: invalid bound specification for a list partition +LINE 1: ...list_parted ATTACH PARTITION fail_part FOR VALUES FROM (1) T... + ^ +DROP TABLE fail_part; +-- check that the table being attached exists +ALTER TABLE list_parted ATTACH PARTITION nonexistant FOR VALUES IN (1); +ERROR: relation "nonexistant" does not exist +-- check ownership of the source table +CREATE ROLE regress_test_me; +CREATE ROLE regress_test_not_me; +CREATE TABLE not_owned_by_me (LIKE list_parted); +ALTER TABLE not_owned_by_me OWNER TO regress_test_not_me; +SET SESSION AUTHORIZATION regress_test_me; +CREATE TABLE owned_by_me ( + a int +) PARTITION BY LIST (a); +ALTER TABLE owned_by_me ATTACH PARTITION not_owned_by_me FOR VALUES IN (1); +ERROR: must be owner of relation not_owned_by_me +RESET SESSION AUTHORIZATION; +DROP TABLE owned_by_me, not_owned_by_me; +DROP ROLE regress_test_not_me; +DROP ROLE regress_test_me; +-- check that the table being attached is not part of regular inheritance +CREATE TABLE parent (LIKE list_parted); +CREATE TABLE child () INHERITS (parent); +ALTER TABLE list_parted ATTACH PARTITION child FOR VALUES IN (1); +ERROR: cannot attach inheritance child as partition +ALTER TABLE list_parted ATTACH PARTITION parent FOR VALUES IN (1); +ERROR: cannot attach inheritance parent as partition +DROP TABLE parent CASCADE; +NOTICE: drop cascades to table child +-- check that the table being attached is not a typed table +CREATE TYPE mytype AS (a int); +CREATE TABLE fail_part OF mytype; +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: cannot attach a typed table as partition +DROP TYPE mytype CASCADE; +NOTICE: drop cascades to table fail_part +-- check existence (or non-existence) of oid column +ALTER TABLE list_parted SET WITH OIDS; +CREATE TABLE fail_part (a int); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: cannot attach table "fail_part" without OIDs as partition of table "list_parted" with OIDs +ALTER TABLE list_parted SET WITHOUT OIDS; +ALTER TABLE fail_part SET WITH OIDS; +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: cannot attach table "fail_part" with OIDs as partition of table "list_parted" without OIDs +DROP TABLE fail_part; +-- check that the table being attached has only columns present in the parent +CREATE TABLE fail_part (like list_parted, c int); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: table "fail_part" contains column "c" not found in parent "list_parted" +DETAIL: New partition should contain only the columns present in parent. +DROP TABLE fail_part; +-- check that the table being attached has every column of the parent +CREATE TABLE fail_part (a int NOT NULL); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: child table is missing column "b" +DROP TABLE fail_part; +-- check that columns match in type, collation and NOT NULL status +CREATE TABLE fail_part ( + b char(3), + a int NOT NULL +); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: child table "fail_part" has different type for column "b" +ALTER TABLE fail_part ALTER b TYPE char (2) COLLATE "en_CA"; +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: child table "fail_part" has different collation for column "b" +DROP TABLE fail_part; +-- check that the table being attached has all constraints of the parent +CREATE TABLE fail_part ( + b char(2) COLLATE "en_US", + a int NOT NULL +); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: child table is missing constraint "check_a" +-- check that the constraint matches in definition with parent's constraint +ALTER TABLE fail_part ADD CONSTRAINT check_a CHECK (a >= 0); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: child table "fail_part" has different definition for check constraint "check_a" +DROP TABLE fail_part; +-- check the attributes and constraints after partition is attached +CREATE TABLE part_1 ( + a int NOT NULL, + b char(2) COLLATE "en_US", + CONSTRAINT check_a CHECK (a > 0) +); +ALTER TABLE list_parted ATTACH PARTITION part_1 FOR VALUES IN (1); +-- attislocal and conislocal are always false for merged attributes and constraints respectively. +SELECT attislocal, attinhcount FROM pg_attribute WHERE attrelid = 'part_1'::regclass AND attnum > 0; + attislocal | attinhcount +------------+------------- + f | 1 + f | 1 +(2 rows) + +SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_1'::regclass AND conname = 'check_a'; + conislocal | coninhcount +------------+------------- + f | 1 +(1 row) + +-- check that the new partition won't overlap with an existing partition +CREATE TABLE fail_part (LIKE part_1 INCLUDING CONSTRAINTS); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ERROR: partition "fail_part" would overlap partition "part_1" +-- check validation when attaching list partitions +CREATE TABLE list_parted2 ( + a int, + b char +) PARTITION BY LIST (a); +-- check that violating rows are correctly reported +CREATE TABLE part_2 (LIKE list_parted2); +INSERT INTO part_2 VALUES (3, 'a'); +ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); +ERROR: partition constraint is violated by some row +-- should be ok after deleting the bad row +DELETE FROM part_2; +ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); +-- adding constraints that describe the desired partition constraint +-- (or more restrictive) will help skip the validation scan +CREATE TABLE part_3_4 ( + LIKE list_parted2, + CONSTRAINT check_a CHECK (a IN (3)) +); +-- however, if a list partition does not accept nulls, there should be +-- an explicit NOT NULL constraint on the partition key column for the +-- validation scan to be skipped; +ALTER TABLE list_parted2 ATTACH PARTITION part_3_4 FOR VALUES IN (3, 4); +-- adding a NOT NULL constraint will cause the scan to be skipped +ALTER TABLE list_parted2 DETACH PARTITION part_3_4; +ALTER TABLE part_3_4 ALTER a SET NOT NULL; +ALTER TABLE list_parted2 ATTACH PARTITION part_3_4 FOR VALUES IN (3, 4); +NOTICE: skipping scan to validate partition constraint +-- check validation when attaching range partitions +CREATE TABLE range_parted ( + a int, + b int +) PARTITION BY RANGE (a, b); +-- check that violating rows are correctly reported +CREATE TABLE part1 ( + a int NOT NULL CHECK (a = 1), + b int NOT NULL CHECK (b >= 1 AND b <= 10) +); +INSERT INTO part1 VALUES (1, 10); +-- Remember the TO bound is exclusive +ALTER TABLE range_parted ATTACH PARTITION part1 FOR VALUES FROM (1, 1) TO (1, 10); +ERROR: partition constraint is violated by some row +-- should be ok after deleting the bad row +DELETE FROM part1; +ALTER TABLE range_parted ATTACH PARTITION part1 FOR VALUES FROM (1, 1) TO (1, 10); +-- adding constraints that describe the desired partition constraint +-- (or more restrictive) will help skip the validation scan +CREATE TABLE part2 ( + a int NOT NULL CHECK (a = 1), + b int NOT NULL CHECK (b >= 10 AND b < 18) +); +ALTER TABLE range_parted ATTACH PARTITION part2 FOR VALUES FROM (1, 10) TO (1, 20); +NOTICE: skipping scan to validate partition constraint +-- check that leaf partitions are scanned when attaching a partitioned +-- table +CREATE TABLE part_5 ( + LIKE list_parted2 +) PARTITION BY LIST (b); +-- check that violating rows are correctly reported +CREATE TABLE part_5_a PARTITION OF part_5 FOR VALUES IN ('a'); +INSERT INTO part_5_a (a, b) VALUES (6, 'a'); +ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5); +ERROR: partition constraint is violated by some row +-- delete the faulting row and also add a constraint to skip the scan +DELETE FROM part_5_a WHERE a NOT IN (3); +ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5)), ALTER a SET NOT NULL; +ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5); +NOTICE: skipping scan to validate partition constraint +-- check that the table being attached is not already a partition +ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); +ERROR: "part_2" is already a partition +-- check that circular inheritance is not allowed +ALTER TABLE part_5 ATTACH PARTITION list_parted2 FOR VALUES IN ('b'); +ERROR: circular inheritance not allowed +DETAIL: "part_5" is already a child of "list_parted2". +ALTER TABLE list_parted2 ATTACH PARTITION list_parted2 FOR VALUES IN (0); +ERROR: circular inheritance not allowed +DETAIL: "list_parted2" is already a child of "list_parted2". +-- +-- DETACH PARTITION +-- +-- check that the partition being detached exists at all +ALTER TABLE list_parted2 DETACH PARTITION part_4; +ERROR: relation "part_4" does not exist +-- check that the partition being detached is actually a partition of the parent +CREATE TABLE not_a_part (a int); +ALTER TABLE list_parted2 DETACH PARTITION not_a_part; +ERROR: relation "not_a_part" is not a partition of relation "list_parted2" +ALTER TABLE list_parted2 DETACH PARTITION part_1; +ERROR: relation "part_1" is not a partition of relation "list_parted2" +-- check that, after being detached, attinhcount/coninhcount is dropped to 0 and +-- attislocal/conislocal is set to true +ALTER TABLE list_parted2 DETACH PARTITION part_3_4; +SELECT attinhcount, attislocal FROM pg_attribute WHERE attrelid = 'part_3_4'::regclass AND attnum > 0; + attinhcount | attislocal +-------------+------------ + 0 | t + 0 | t +(2 rows) + +SELECT coninhcount, conislocal FROM pg_constraint WHERE conrelid = 'part_3_4'::regclass AND conname = 'check_a'; + coninhcount | conislocal +-------------+------------ + 0 | t +(1 row) + +DROP TABLE part_3_4; +-- Check ALTER TABLE commands for partitioned tables and partitions +-- cannot add/drop column to/from *only* the parent +ALTER TABLE ONLY list_parted2 ADD COLUMN c int; +ERROR: column must be added to child tables too +ALTER TABLE ONLY list_parted2 DROP COLUMN b; +ERROR: column must be dropped from child tables too +-- cannot add a column to partition or drop an inherited one +ALTER TABLE part_2 ADD COLUMN c text; +ERROR: cannot add column to a partition +ALTER TABLE part_2 DROP COLUMN b; +ERROR: cannot drop inherited column "b" +-- Nor rename, alter type +ALTER TABLE part_2 RENAME COLUMN b to c; +ERROR: cannot rename inherited column "b" +ALTER TABLE part_2 ALTER COLUMN b TYPE text; +ERROR: cannot alter inherited column "b" +-- cannot add NOT NULL or check constraints to *only* the parent (ie, non-inherited) +ALTER TABLE ONLY list_parted2 ALTER b SET NOT NULL; +ERROR: constraint must be added to child tables too +ALTER TABLE ONLY list_parted2 add constraint check_b check (b <> 'zz'); +ERROR: constraint must be added to child tables too +ALTER TABLE list_parted2 add constraint check_b check (b <> 'zz') NO INHERIT; +ERROR: cannot add NO INHERIT constraint to partitioned table "list_parted2" +-- cannot drop inherited NOT NULL or check constraints from partition +ALTER TABLE list_parted2 ALTER b SET NOT NULL, ADD CONSTRAINT check_a2 CHECK (a > 0); +ALTER TABLE part_2 ALTER b DROP NOT NULL; +ERROR: column "b" is marked NOT NULL in parent table +ALTER TABLE part_2 DROP CONSTRAINT check_a2; +ERROR: cannot drop inherited constraint "check_a2" of relation "part_2" +-- cannot drop NOT NULL or check constraints from *only* the parent +ALTER TABLE ONLY list_parted2 ALTER a DROP NOT NULL; +ERROR: constraint must be dropped from child tables too +ALTER TABLE ONLY list_parted2 DROP CONSTRAINT check_a2; +ERROR: constraint must be dropped from child tables too +-- check that a partition cannot participate in regular inheritance +CREATE TABLE inh_test () INHERITS (part_2); +ERROR: cannot inherit from partition "part_2" +CREATE TABLE inh_test (LIKE part_2); +ALTER TABLE inh_test INHERIT part_2; +ERROR: cannot inherit from a partition +ALTER TABLE part_2 INHERIT inh_test; +ERROR: cannot change inheritance of a partition +-- cannot drop or alter type of partition key columns of lower level +-- partitioned tables; for example, part_5, which is list_parted2's +-- partition, is partitioned on b; +ALTER TABLE list_parted2 DROP COLUMN b; +ERROR: cannot drop column named in partition key +ALTER TABLE list_parted2 ALTER COLUMN b TYPE text; +ERROR: cannot alter type of column named in partition key +-- cleanup +DROP TABLE list_parted, list_parted2, range_parted CASCADE; +NOTICE: drop cascades to 6 other objects +DETAIL: drop cascades to table part1 +drop cascades to table part2 +drop cascades to table part_2 +drop cascades to table part_5 +drop cascades to table part_5_a +drop cascades to table part_1 diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out index 02e0720..783143d 100644 --- a/src/test/regress/expected/create_table.out +++ b/src/test/regress/expected/create_table.out @@ -439,3 +439,190 @@ Partition key: RANGE (a oid_ops, plusone(b), c, d COLLATE "en_US") Partition key: LIST ((a + 1)) DROP TABLE partitioned, partitioned2; +-- +-- Partitions +-- +-- check partition bound syntax +CREATE TABLE list_parted ( + a int +) PARTITION BY LIST (a); +-- syntax allows only string literal, numeric literal and null to be +-- specified for a partition bound value +CREATE TABLE part_1 PARTITION OF list_parted FOR VALUES IN ('1'); +CREATE TABLE part_2 PARTITION OF list_parted FOR VALUES IN (2); +CREATE TABLE part_null PARTITION OF list_parted FOR VALUES IN (null); +CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN (int '1'); +ERROR: syntax error at or near "int" +LINE 1: ... fail_part PARTITION OF list_parted FOR VALUES IN (int '1'); + ^ +CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN ('1'::int); +ERROR: syntax error at or near "::" +LINE 1: ...fail_part PARTITION OF list_parted FOR VALUES IN ('1'::int); + ^ +-- syntax does not allow empty list of values for list partitions +CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN (); +ERROR: syntax error at or near ")" +LINE 1: ...E TABLE fail_part PARTITION OF list_parted FOR VALUES IN (); + ^ +-- trying to specify range for list partitioned table +CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES FROM (1) TO (2); +ERROR: invalid bound specification for a list partition +LINE 1: ...BLE fail_part PARTITION OF list_parted FOR VALUES FROM (1) T... + ^ +-- specified literal can't be cast to the partition column data type +CREATE TABLE bools ( + a bool +) PARTITION BY LIST (a); +CREATE TABLE bools_true PARTITION OF bools FOR VALUES IN (1); +ERROR: specified value cannot be cast to type "boolean" of column "a" +LINE 1: ...REATE TABLE bools_true PARTITION OF bools FOR VALUES IN (1); + ^ +DROP TABLE bools; +CREATE TABLE range_parted ( + a date +) PARTITION BY RANGE (a); +-- trying to specify list for range partitioned table +CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES IN ('a'); +ERROR: invalid bound specification for a range partition +LINE 1: ...BLE fail_part PARTITION OF range_parted FOR VALUES IN ('a'); + ^ +-- each of start and end bounds must have same number of values as the +-- length of the partition key +CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('z'); +ERROR: FROM must specify exactly one value per partitioning column +CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1); +ERROR: TO must specify exactly one value per partitioning column +-- cannot specify null values in range bounds +CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded); +ERROR: cannot specify NULL in range bound +-- check if compatible with the specified parent +-- cannot create as partition of a non-partitioned table +CREATE TABLE unparted ( + a int +); +CREATE TABLE fail_part PARTITION OF unparted FOR VALUES IN ('a'); +ERROR: "unparted" is not partitioned +DROP TABLE unparted; +-- cannot create a permanent rel as partition of a temp rel +CREATE TEMP TABLE temp_parted ( + a int +) PARTITION BY LIST (a); +CREATE TABLE fail_part PARTITION OF temp_parted FOR VALUES IN ('a'); +ERROR: cannot create as partition of temporary relation "temp_parted" +DROP TABLE temp_parted; +-- cannot create a table with oids as partition of table without oids +CREATE TABLE no_oids_parted ( + a int +) PARTITION BY RANGE (a) WITHOUT OIDS; +CREATE TABLE fail_part PARTITION OF no_oids_parted FOR VALUES FROM (1) TO (10 )WITH OIDS; +ERROR: cannot create table with OIDs as partition of table without OIDs +DROP TABLE no_oids_parted; +-- likewise, the reverse if also true +CREATE TABLE oids_parted ( + a int +) PARTITION BY RANGE (a) WITH OIDS; +CREATE TABLE fail_part PARTITION OF oids_parted FOR VALUES FROM (1) TO (10 ) WITHOUT OIDS; +ERROR: cannot create table without OIDs as partition of table with OIDs +DROP TABLE oids_parted; +-- check for partition bound overlap and other invalid specifications +CREATE TABLE list_parted2 ( + a varchar +) PARTITION BY LIST (a); +CREATE TABLE part_null_z PARTITION OF list_parted2 FOR VALUES IN (null, 'z'); +CREATE TABLE part_ab PARTITION OF list_parted2 FOR VALUES IN ('a', 'b'); +CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN (null); +ERROR: partition "fail_part" would overlap partition "part_null_z" +CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN ('b', 'c'); +ERROR: partition "fail_part" would overlap partition "part_ab" +CREATE TABLE range_parted2 ( + a int +) PARTITION BY RANGE (a); +-- trying to create range partition with empty range +CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (0); +ERROR: cannot create range partition with empty range +-- note that the range '[1, 1)' has no elements +CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1); +ERROR: cannot create range partition with empty range +CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1); +CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2); +ERROR: partition "fail_part" would overlap partition "part0" +CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10); +CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded); +ERROR: partition "fail_part" would overlap partition "part1" +-- now check for multi-column range partition key +CREATE TABLE range_parted3 ( + a int, + b int +) PARTITION BY RANGE (a, (b+1)); +CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded); +CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1); +ERROR: partition "fail_part" would overlap partition "part00" +CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1); +CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10); +CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded); +CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, 20); +ERROR: partition "fail_part" would overlap partition "part12" +-- cannot create a partition that says column b is allowed to range +-- from -infinity to +infinity, while there exist partitions that have +-- more specific ranges +CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded); +ERROR: partition "fail_part" would overlap partition "part10" +-- check schema propagation from parent +CREATE TABLE parted ( + a text, + b int NOT NULL DEFAULT 0, + CONSTRAINT check_a CHECK (length(a) > 0) +) PARTITION BY LIST (a); +CREATE TABLE part_a PARTITION OF parted FOR VALUES IN ('a'); +-- only inherited attributes (never local ones) +SELECT attname, attislocal, attinhcount FROM pg_attribute WHERE attrelid = 'part_a'::regclass and attnum > 0; + attname | attislocal | attinhcount +---------+------------+------------- + a | f | 1 + b | f | 1 +(2 rows) + +-- able to specify column default, column constraint, and table constraint +CREATE TABLE part_b PARTITION OF parted ( + b WITH OPTIONS NOT NULL DEFAULT 1 CHECK (b >= 0), + CONSTRAINT check_a CHECK (length(a) > 0) +) FOR VALUES IN ('b'); +NOTICE: merging constraint "check_a" with inherited definition +-- conislocal should be false for any merged constraints +SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass AND conname = 'check_a'; + conislocal | coninhcount +------------+------------- + f | 1 +(1 row) + +-- specify PARTITION BY for a partition +CREATE TABLE fail_part_col_not_found PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE (c); +ERROR: column "c" named in partition key does not exist +CREATE TABLE part_c PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE ((b)); +-- create a level-2 partition +CREATE TABLE part_c_1_10 PARTITION OF part_c FOR VALUES FROM (1) TO (10); +-- partitions cannot be dropped directly +DROP TABLE part_a; +-- need to specify CASCADE to drop partitions along with the parent +DROP TABLE parted; +ERROR: cannot drop table parted because other objects depend on it +DETAIL: table part_b depends on table parted +table part_c depends on table parted +table part_c_1_10 depends on table part_c +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE parted, list_parted, range_parted, list_parted2, range_parted2, range_parted3 CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to table part00 +drop cascades to table part10 +drop cascades to table part11 +drop cascades to table part12 +drop cascades to table part0 +drop cascades to table part1 +drop cascades to table part_null_z +drop cascades to table part_ab +drop cascades to table part_1 +drop cascades to table part_2 +drop cascades to table part_null +drop cascades to table part_b +drop cascades to table part_c +drop cascades to table part_c_1_10 diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index ec61b02..901c8e7 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -1907,3 +1907,259 @@ ALTER TABLE foo INHERIT partitioned; ALTER TABLE partitioned ADD CONSTRAINT chk_a CHECK (a > 0) NO INHERIT; DROP TABLE partitioned, foo; + +-- +-- ATTACH PARTITION +-- + +-- check that target table is partitioned +CREATE TABLE unparted ( + a int +); +CREATE TABLE fail_part (like unparted); +ALTER TABLE unparted ATTACH PARTITION fail_part FOR VALUES IN ('a'); +DROP TABLE unparted, fail_part; + +-- check that partition bound is compatible +CREATE TABLE list_parted ( + a int NOT NULL, + b char(2) COLLATE "en_US", + CONSTRAINT check_a CHECK (a > 0) +) PARTITION BY LIST (a); +CREATE TABLE fail_part (LIKE list_parted); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES FROM (1) TO (10); +DROP TABLE fail_part; + +-- check that the table being attached exists +ALTER TABLE list_parted ATTACH PARTITION nonexistant FOR VALUES IN (1); + +-- check ownership of the source table +CREATE ROLE regress_test_me; +CREATE ROLE regress_test_not_me; +CREATE TABLE not_owned_by_me (LIKE list_parted); +ALTER TABLE not_owned_by_me OWNER TO regress_test_not_me; +SET SESSION AUTHORIZATION regress_test_me; +CREATE TABLE owned_by_me ( + a int +) PARTITION BY LIST (a); +ALTER TABLE owned_by_me ATTACH PARTITION not_owned_by_me FOR VALUES IN (1); +RESET SESSION AUTHORIZATION; +DROP TABLE owned_by_me, not_owned_by_me; +DROP ROLE regress_test_not_me; +DROP ROLE regress_test_me; + +-- check that the table being attached is not part of regular inheritance +CREATE TABLE parent (LIKE list_parted); +CREATE TABLE child () INHERITS (parent); +ALTER TABLE list_parted ATTACH PARTITION child FOR VALUES IN (1); +ALTER TABLE list_parted ATTACH PARTITION parent FOR VALUES IN (1); +DROP TABLE parent CASCADE; + +-- check that the table being attached is not a typed table +CREATE TYPE mytype AS (a int); +CREATE TABLE fail_part OF mytype; +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +DROP TYPE mytype CASCADE; + +-- check existence (or non-existence) of oid column +ALTER TABLE list_parted SET WITH OIDS; +CREATE TABLE fail_part (a int); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); + +ALTER TABLE list_parted SET WITHOUT OIDS; +ALTER TABLE fail_part SET WITH OIDS; +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +DROP TABLE fail_part; + +-- check that the table being attached has only columns present in the parent +CREATE TABLE fail_part (like list_parted, c int); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +DROP TABLE fail_part; + +-- check that the table being attached has every column of the parent +CREATE TABLE fail_part (a int NOT NULL); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +DROP TABLE fail_part; + +-- check that columns match in type, collation and NOT NULL status +CREATE TABLE fail_part ( + b char(3), + a int NOT NULL +); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +ALTER TABLE fail_part ALTER b TYPE char (2) COLLATE "en_CA"; +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +DROP TABLE fail_part; + +-- check that the table being attached has all constraints of the parent +CREATE TABLE fail_part ( + b char(2) COLLATE "en_US", + a int NOT NULL +); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); + +-- check that the constraint matches in definition with parent's constraint +ALTER TABLE fail_part ADD CONSTRAINT check_a CHECK (a >= 0); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); +DROP TABLE fail_part; + +-- check the attributes and constraints after partition is attached +CREATE TABLE part_1 ( + a int NOT NULL, + b char(2) COLLATE "en_US", + CONSTRAINT check_a CHECK (a > 0) +); +ALTER TABLE list_parted ATTACH PARTITION part_1 FOR VALUES IN (1); +-- attislocal and conislocal are always false for merged attributes and constraints respectively. +SELECT attislocal, attinhcount FROM pg_attribute WHERE attrelid = 'part_1'::regclass AND attnum > 0; +SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_1'::regclass AND conname = 'check_a'; + +-- check that the new partition won't overlap with an existing partition +CREATE TABLE fail_part (LIKE part_1 INCLUDING CONSTRAINTS); +ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); + +-- check validation when attaching list partitions +CREATE TABLE list_parted2 ( + a int, + b char +) PARTITION BY LIST (a); + +-- check that violating rows are correctly reported +CREATE TABLE part_2 (LIKE list_parted2); +INSERT INTO part_2 VALUES (3, 'a'); +ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); + +-- should be ok after deleting the bad row +DELETE FROM part_2; +ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); + +-- adding constraints that describe the desired partition constraint +-- (or more restrictive) will help skip the validation scan +CREATE TABLE part_3_4 ( + LIKE list_parted2, + CONSTRAINT check_a CHECK (a IN (3)) +); + +-- however, if a list partition does not accept nulls, there should be +-- an explicit NOT NULL constraint on the partition key column for the +-- validation scan to be skipped; +ALTER TABLE list_parted2 ATTACH PARTITION part_3_4 FOR VALUES IN (3, 4); + +-- adding a NOT NULL constraint will cause the scan to be skipped +ALTER TABLE list_parted2 DETACH PARTITION part_3_4; +ALTER TABLE part_3_4 ALTER a SET NOT NULL; +ALTER TABLE list_parted2 ATTACH PARTITION part_3_4 FOR VALUES IN (3, 4); + + +-- check validation when attaching range partitions +CREATE TABLE range_parted ( + a int, + b int +) PARTITION BY RANGE (a, b); + +-- check that violating rows are correctly reported +CREATE TABLE part1 ( + a int NOT NULL CHECK (a = 1), + b int NOT NULL CHECK (b >= 1 AND b <= 10) +); +INSERT INTO part1 VALUES (1, 10); +-- Remember the TO bound is exclusive +ALTER TABLE range_parted ATTACH PARTITION part1 FOR VALUES FROM (1, 1) TO (1, 10); + +-- should be ok after deleting the bad row +DELETE FROM part1; +ALTER TABLE range_parted ATTACH PARTITION part1 FOR VALUES FROM (1, 1) TO (1, 10); + +-- adding constraints that describe the desired partition constraint +-- (or more restrictive) will help skip the validation scan +CREATE TABLE part2 ( + a int NOT NULL CHECK (a = 1), + b int NOT NULL CHECK (b >= 10 AND b < 18) +); +ALTER TABLE range_parted ATTACH PARTITION part2 FOR VALUES FROM (1, 10) TO (1, 20); + +-- check that leaf partitions are scanned when attaching a partitioned +-- table +CREATE TABLE part_5 ( + LIKE list_parted2 +) PARTITION BY LIST (b); + +-- check that violating rows are correctly reported +CREATE TABLE part_5_a PARTITION OF part_5 FOR VALUES IN ('a'); +INSERT INTO part_5_a (a, b) VALUES (6, 'a'); +ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5); + +-- delete the faulting row and also add a constraint to skip the scan +DELETE FROM part_5_a WHERE a NOT IN (3); +ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5)), ALTER a SET NOT NULL; +ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5); + + +-- check that the table being attached is not already a partition +ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); + +-- check that circular inheritance is not allowed +ALTER TABLE part_5 ATTACH PARTITION list_parted2 FOR VALUES IN ('b'); +ALTER TABLE list_parted2 ATTACH PARTITION list_parted2 FOR VALUES IN (0); + +-- +-- DETACH PARTITION +-- + +-- check that the partition being detached exists at all +ALTER TABLE list_parted2 DETACH PARTITION part_4; + +-- check that the partition being detached is actually a partition of the parent +CREATE TABLE not_a_part (a int); +ALTER TABLE list_parted2 DETACH PARTITION not_a_part; +ALTER TABLE list_parted2 DETACH PARTITION part_1; + +-- check that, after being detached, attinhcount/coninhcount is dropped to 0 and +-- attislocal/conislocal is set to true +ALTER TABLE list_parted2 DETACH PARTITION part_3_4; +SELECT attinhcount, attislocal FROM pg_attribute WHERE attrelid = 'part_3_4'::regclass AND attnum > 0; +SELECT coninhcount, conislocal FROM pg_constraint WHERE conrelid = 'part_3_4'::regclass AND conname = 'check_a'; +DROP TABLE part_3_4; + +-- Check ALTER TABLE commands for partitioned tables and partitions + +-- cannot add/drop column to/from *only* the parent +ALTER TABLE ONLY list_parted2 ADD COLUMN c int; +ALTER TABLE ONLY list_parted2 DROP COLUMN b; + +-- cannot add a column to partition or drop an inherited one +ALTER TABLE part_2 ADD COLUMN c text; +ALTER TABLE part_2 DROP COLUMN b; + +-- Nor rename, alter type +ALTER TABLE part_2 RENAME COLUMN b to c; +ALTER TABLE part_2 ALTER COLUMN b TYPE text; + +-- cannot add NOT NULL or check constraints to *only* the parent (ie, non-inherited) +ALTER TABLE ONLY list_parted2 ALTER b SET NOT NULL; +ALTER TABLE ONLY list_parted2 add constraint check_b check (b <> 'zz'); +ALTER TABLE list_parted2 add constraint check_b check (b <> 'zz') NO INHERIT; + +-- cannot drop inherited NOT NULL or check constraints from partition +ALTER TABLE list_parted2 ALTER b SET NOT NULL, ADD CONSTRAINT check_a2 CHECK (a > 0); +ALTER TABLE part_2 ALTER b DROP NOT NULL; +ALTER TABLE part_2 DROP CONSTRAINT check_a2; + +-- cannot drop NOT NULL or check constraints from *only* the parent +ALTER TABLE ONLY list_parted2 ALTER a DROP NOT NULL; +ALTER TABLE ONLY list_parted2 DROP CONSTRAINT check_a2; + +-- check that a partition cannot participate in regular inheritance +CREATE TABLE inh_test () INHERITS (part_2); +CREATE TABLE inh_test (LIKE part_2); +ALTER TABLE inh_test INHERIT part_2; +ALTER TABLE part_2 INHERIT inh_test; + +-- cannot drop or alter type of partition key columns of lower level +-- partitioned tables; for example, part_5, which is list_parted2's +-- partition, is partitioned on b; +ALTER TABLE list_parted2 DROP COLUMN b; +ALTER TABLE list_parted2 ALTER COLUMN b TYPE text; + +-- cleanup +DROP TABLE list_parted, list_parted2, range_parted CASCADE; diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql index 2af3214..8696a85 100644 --- a/src/test/regress/sql/create_table.sql +++ b/src/test/regress/sql/create_table.sql @@ -419,3 +419,156 @@ CREATE TABLE fail () INHERITS (partitioned2); \d partitioned2 DROP TABLE partitioned, partitioned2; + +-- +-- Partitions +-- + +-- check partition bound syntax + +CREATE TABLE list_parted ( + a int +) PARTITION BY LIST (a); +-- syntax allows only string literal, numeric literal and null to be +-- specified for a partition bound value +CREATE TABLE part_1 PARTITION OF list_parted FOR VALUES IN ('1'); +CREATE TABLE part_2 PARTITION OF list_parted FOR VALUES IN (2); +CREATE TABLE part_null PARTITION OF list_parted FOR VALUES IN (null); +CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN (int '1'); +CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN ('1'::int); + +-- syntax does not allow empty list of values for list partitions +CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN (); +-- trying to specify range for list partitioned table +CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES FROM (1) TO (2); + +-- specified literal can't be cast to the partition column data type +CREATE TABLE bools ( + a bool +) PARTITION BY LIST (a); +CREATE TABLE bools_true PARTITION OF bools FOR VALUES IN (1); +DROP TABLE bools; + +CREATE TABLE range_parted ( + a date +) PARTITION BY RANGE (a); + +-- trying to specify list for range partitioned table +CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES IN ('a'); +-- each of start and end bounds must have same number of values as the +-- length of the partition key +CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('z'); +CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1); + +-- cannot specify null values in range bounds +CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded); + +-- check if compatible with the specified parent + +-- cannot create as partition of a non-partitioned table +CREATE TABLE unparted ( + a int +); +CREATE TABLE fail_part PARTITION OF unparted FOR VALUES IN ('a'); +DROP TABLE unparted; + +-- cannot create a permanent rel as partition of a temp rel +CREATE TEMP TABLE temp_parted ( + a int +) PARTITION BY LIST (a); +CREATE TABLE fail_part PARTITION OF temp_parted FOR VALUES IN ('a'); +DROP TABLE temp_parted; + +-- cannot create a table with oids as partition of table without oids +CREATE TABLE no_oids_parted ( + a int +) PARTITION BY RANGE (a) WITHOUT OIDS; +CREATE TABLE fail_part PARTITION OF no_oids_parted FOR VALUES FROM (1) TO (10 )WITH OIDS; +DROP TABLE no_oids_parted; + +-- likewise, the reverse if also true +CREATE TABLE oids_parted ( + a int +) PARTITION BY RANGE (a) WITH OIDS; +CREATE TABLE fail_part PARTITION OF oids_parted FOR VALUES FROM (1) TO (10 ) WITHOUT OIDS; +DROP TABLE oids_parted; + +-- check for partition bound overlap and other invalid specifications + +CREATE TABLE list_parted2 ( + a varchar +) PARTITION BY LIST (a); +CREATE TABLE part_null_z PARTITION OF list_parted2 FOR VALUES IN (null, 'z'); +CREATE TABLE part_ab PARTITION OF list_parted2 FOR VALUES IN ('a', 'b'); + +CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN (null); +CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN ('b', 'c'); + +CREATE TABLE range_parted2 ( + a int +) PARTITION BY RANGE (a); + +-- trying to create range partition with empty range +CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (0); +-- note that the range '[1, 1)' has no elements +CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1); + +CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1); +CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2); +CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10); +CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded); + +-- now check for multi-column range partition key +CREATE TABLE range_parted3 ( + a int, + b int +) PARTITION BY RANGE (a, (b+1)); + +CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded); +CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1); + +CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1); +CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10); +CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded); +CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, 20); + +-- cannot create a partition that says column b is allowed to range +-- from -infinity to +infinity, while there exist partitions that have +-- more specific ranges +CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded); + +-- check schema propagation from parent + +CREATE TABLE parted ( + a text, + b int NOT NULL DEFAULT 0, + CONSTRAINT check_a CHECK (length(a) > 0) +) PARTITION BY LIST (a); + +CREATE TABLE part_a PARTITION OF parted FOR VALUES IN ('a'); + +-- only inherited attributes (never local ones) +SELECT attname, attislocal, attinhcount FROM pg_attribute WHERE attrelid = 'part_a'::regclass and attnum > 0; + +-- able to specify column default, column constraint, and table constraint +CREATE TABLE part_b PARTITION OF parted ( + b WITH OPTIONS NOT NULL DEFAULT 1 CHECK (b >= 0), + CONSTRAINT check_a CHECK (length(a) > 0) +) FOR VALUES IN ('b'); +-- conislocal should be false for any merged constraints +SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass AND conname = 'check_a'; + +-- specify PARTITION BY for a partition +CREATE TABLE fail_part_col_not_found PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE (c); +CREATE TABLE part_c PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE ((b)); + +-- create a level-2 partition +CREATE TABLE part_c_1_10 PARTITION OF part_c FOR VALUES FROM (1) TO (10); + +-- partitions cannot be dropped directly +DROP TABLE part_a; + +-- need to specify CASCADE to drop partitions along with the parent +DROP TABLE parted; + +DROP TABLE parted, list_parted, range_parted, list_parted2, range_parted2, range_parted3 CASCADE; -- 1.7.1