From 695ba090adabb015ac9f33b73fd3b09318f9206e Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Tue, 10 Feb 2026 18:05:20 -0300 Subject: [PATCH v3 2/4] Add partition table support to CREATE SCHEMA ... LIKE This commit extends CREATE SCHEMA ... LIKE to properly handle partitioned tables and their partitions: - Partitioned tables are created with their partition specification (RANGE, LIST, or HASH) preserved by reconstructing PartitionSpec from pg_partitioned_table. - Partitions within the same source schema are attached to their parent table in the new schema by setting inhRelations and partbound in the CreateStmt. - Partitions whose parent table is in a different schema ("orphan partitions") are skipped with a WARNING, since the partition relationship cannot be recreated. - Statement ordering ensures partitioned tables are created before their partitions. The transformPartitionBound() function in parse_utilcmd.c is modified to detect already transformed partition bounds (retrieved from pg_class. relpartbound) and skip transformation, since these contain Const nodes rather than raw expressions. --- src/backend/commands/schemacmds.c | 276 +++++++++++++++++++- src/backend/commands/tablecmds.c | 3 + src/backend/parser/parse_utilcmd.c | 9 + src/include/nodes/parsenodes.h | 4 + src/test/regress/expected/create_schema.out | 134 ++++++++++ src/test/regress/sql/create_schema.sql | 103 ++++++++ 6 files changed, 519 insertions(+), 10 deletions(-) diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c index 4c6edfa3da6..d28e3429266 100644 --- a/src/backend/commands/schemacmds.c +++ b/src/backend/commands/schemacmds.c @@ -15,6 +15,7 @@ #include "postgres.h" #include "access/genam.h" +#include "access/relation.h" #include "access/htup_details.h" #include "access/table.h" #include "access/xact.h" @@ -22,6 +23,8 @@ #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/namespace.h" +#include "catalog/partition.h" +#include "catalog/pg_partitioned_table.h" #include "catalog/objectaccess.h" #include "catalog/pg_authid.h" #include "catalog/pg_database.h" @@ -30,6 +33,7 @@ #include "commands/schemacmds.h" #include "miscadmin.h" #include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" #include "parser/parse_utilcmd.h" #include "parser/scansup.h" #include "tcop/utility.h" @@ -38,24 +42,200 @@ #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/ruleutils.h" #include "utils/syscache.h" static void AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId); static List *collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, bits32 options); +static PartitionSpec *buildPartitionSpecForRelation(Oid relid); +static PartitionBoundSpec *getPartitionBoundSpec(Oid partOid); + +/* Returns a PartitionBoundSpec node for the given partition OID. */ +static PartitionBoundSpec * +getPartitionBoundSpec(Oid partOid) +{ + HeapTuple tuple; + Datum datum; + bool isnull; + PartitionBoundSpec *boundspec; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(partOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", partOid); + + datum = SysCacheGetAttr(RELOID, tuple, + Anum_pg_class_relpartbound, + &isnull); + if (isnull) + elog(ERROR, "partition bound for relation %u is null", partOid); + + boundspec = stringToNode(TextDatumGetCString(datum)); + + /* + * stringToNode return a bound spec already transformed for LIST and RANGE + * strategies. Hash bounds only have modulus/remainder as integers + */ + if (!boundspec->is_default && boundspec->strategy != PARTITION_STRATEGY_HASH) + boundspec->is_transformed = true; + + ReleaseSysCache(tuple); + + Assert(IsA(boundspec, PartitionBoundSpec)); + + return boundspec; +} + +/* + * Constructs a PartitionSpec that can be used in a CreateStmt to recreate a + * partitioned table with the same partition key. + */ +static PartitionSpec * +buildPartitionSpecForRelation(Oid relid) +{ + PartitionSpec *partspec; + HeapTuple tuple; + Form_pg_partitioned_table form; + Datum datum; + oidvector *partcollation; + List *partexprs = NIL; + ListCell *partexpr_item; + Relation rel; + TupleDesc tupdesc; + int keyno; + + tuple = SearchSysCache1(PARTRELID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for partition key of %u", relid); + + form = (Form_pg_partitioned_table) GETSTRUCT(tuple); + + /* Get partcollation */ + datum = SysCacheGetAttrNotNull(PARTRELID, tuple, + Anum_pg_partitioned_table_partcollation); + partcollation = (oidvector *) DatumGetPointer(datum); + + /* Get partition expressions if any */ + if (!heap_attisnull(tuple, Anum_pg_partitioned_table_partexprs, NULL)) + { + Datum exprsDatum; + char *exprsString; + + exprsDatum = SysCacheGetAttrNotNull(PARTRELID, tuple, + Anum_pg_partitioned_table_partexprs); + exprsString = TextDatumGetCString(exprsDatum); + partexprs = (List *) stringToNode(exprsString); + + pfree(exprsString); + + Assert(IsA(partexprs, List)); + } + + /* Open relation to get attribute names */ + rel = relation_open(relid, AccessShareLock); + tupdesc = RelationGetDescr(rel); + + /* Build PartitionSpec */ + partspec = makeNode(PartitionSpec); + partspec->strategy = form->partstrat; + partspec->partParams = NIL; + partspec->location = -1; + + partexpr_item = list_head(partexprs); + + for (keyno = 0; keyno < form->partnatts; keyno++) + { + AttrNumber attnum = form->partattrs.values[keyno]; + PartitionElem *pelem; + Oid keycolcollation; + Oid partcoll; + + pelem = makeNode(PartitionElem); + pelem->location = -1; + + if (attnum != 0) + { + /* Simple column reference */ + Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1); + + pelem->name = pstrdup(NameStr(attr->attname)); + pelem->expr = NULL; + keycolcollation = attr->attcollation; + } + else + { + /* + * Expression-based partition key. + * + * The partexprs list contains the partition key expressions in + * their internal node representation, previously extracted from + * pg_partitioned_table.partexprs. We need to convert each + * expression back to SQL text form for use in CREATE TABLE. + */ + Node *partexpr; + + Assert(partexpr_item != NULL); + + /* + * Fetch the current expression and advance the iterator. + * partexprs contains only expressions (not simple column refs), + * so we consume them in order as we encounter expression-based + * partition keys (attnum == 0) while iterating through partattrs. + */ + partexpr = (Node *) lfirst(partexpr_item); + partexpr_item = lnext(partexprs, partexpr_item); + + pelem->name = NULL; + pelem->expr = partexpr; + keycolcollation = exprCollation(partexpr); + + partspec->is_transformed = true; + } + + /* Handle collation */ + partcoll = partcollation->values[keyno]; + if (OidIsValid(partcoll) && partcoll != keycolcollation) + pelem->collation = list_make1(makeString(generate_collation_name(partcoll))); + else + pelem->collation = NIL; + + /* Handle opclass - only include if not default */ + pelem->opclass = NIL; + + partspec->partParams = lappend(partspec->partParams, pelem); + } + + relation_close(rel, AccessShareLock); + ReleaseSysCache(tuple); + + return partspec; +} /* * Subroutine for CREATE SCHEMA LIKE. * - * It return a list of CreateStmt statements for tables that are on source - * schema that should be created on target schema. + * It returns a list of CreateStmt statements for tables that are in the source + * schema that should be created in the target schema. + * + * It uses CREATE TABLE ... LIKE existing infrastructure, with special handling + * for partitioned tables and their partitions: * - * It uses CREATE TABLE ... LIKE existing infrastructure. + * - Partitioned tables are created with their partition specification preserved. + * - Partitions whose parent table is in the same schema are attached to the + * new parent table in the target schema. + * - Partitions whose parent table is in a different schema are skipped with + * a WARNING, since we cannot recreate the partition relationship. + * + * The returned list is ordered so that partitioned tables appear before their + * partitions, ensuring correct creation order. */ static List * collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, bits32 options) { + List *regularTables = NIL; + List *partitionedTables = NIL; + List *partitions = NIL; List *result = NIL; bool preserveIndexNames = false; Relation pg_class; @@ -63,6 +243,10 @@ collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, ScanKeyData key; HeapTuple tuple; bits32 tableOptions; + char *srcSchemaName; + ListCell *lc; + + srcSchemaName = get_namespace_name(srcNspOid); /* * Determine CREATE TABLE LIKE options. We copy most properties by @@ -105,6 +289,7 @@ collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, while (HeapTupleIsValid(tuple = systable_getnext(scan))) { Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple); + Oid relOid = classForm->oid; CreateStmt *createStmt; TableLikeClause *likeClause; RangeVar *newRelation; @@ -115,6 +300,33 @@ collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, classForm->relkind != RELKIND_PARTITIONED_TABLE) continue; + /* + * Check if this is a partition. Partitions need special handling. + */ + if (classForm->relispartition) + { + Oid parentOid; + Oid parentNspOid; + + parentOid = get_partition_parent(relOid, false); + parentNspOid = get_rel_namespace(parentOid); + + /* + * If the parent is in a different schema, skip this partition + * with a warning. We cannot recreate the partition relationship + * since the parent table is not being copied. + */ + if (parentNspOid != srcNspOid) + { + ereport(WARNING, + (errmsg("skipping partition \"%s.%s\" because its parent table is in schema \"%s\"", + srcSchemaName, + NameStr(classForm->relname), + get_namespace_name(parentNspOid)))); + continue; + } + } + createStmt = makeNode(CreateStmt); likeClause = makeNode(TableLikeClause); @@ -125,7 +337,7 @@ collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, newRelation->relpersistence = classForm->relpersistence; /* Source table reference */ - sourceRelation = makeRangeVar(get_namespace_name(srcNspOid), + sourceRelation = makeRangeVar(pstrdup(srcSchemaName), pstrdup(NameStr(classForm->relname)), -1); @@ -144,22 +356,66 @@ collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, createStmt->nnconstraints = NIL; createStmt->options = NIL; createStmt->oncommit = ONCOMMIT_NOOP; - - /* - * XXX: Should we have INCLUDING TABLESPACE? If not, should we use the - * same tablespace of source table? - */ createStmt->tablespacename = NULL; createStmt->accessMethod = NULL; createStmt->if_not_exists = false; + /* + * Handle partitioned tables: preserve the partition specification. + */ + if (classForm->relkind == RELKIND_PARTITIONED_TABLE) + { + createStmt->partspec = buildPartitionSpecForRelation(relOid); + partitionedTables = lappend(partitionedTables, createStmt); + } + + /* + * Handle partitions: set up inheritance and partition bound to attach + * this partition to the parent table in the new schema. + */ + else if (classForm->relispartition) + { + Oid parentOid; + char *parentName; + RangeVar *parent; + + parentOid = get_partition_parent(relOid, false); + parentName = get_rel_name(parentOid); + + /* Reference to parent in new schema */ + parent = makeRangeVar(pstrdup(newSchemaName), + pstrdup(parentName), + -1); + + createStmt->inhRelations = list_make1(parent); + createStmt->partbound = getPartitionBoundSpec(relOid); - result = lappend(result, createStmt); + partitions = lappend(partitions, createStmt); + } + else + { + /* Regular table */ + regularTables = lappend(regularTables, createStmt); + } } systable_endscan(scan); table_close(pg_class, AccessShareLock); + /* + * Build the result list in proper order: regular tables first, then + * partitioned tables, then partitions. This ensures parent tables exist + * before their partitions are created. + */ + foreach(lc, regularTables) + result = lappend(result, lfirst(lc)); + + foreach(lc, partitionedTables) + result = lappend(result, lfirst(lc)); + + foreach(lc, partitions) + result = lappend(result, lfirst(lc)); + return result; } diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 8010fe71a40..554ebb54e04 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -19759,6 +19759,9 @@ transformPartitionSpec(Relation rel, PartitionSpec *partspec) ParseNamespaceItem *nsitem; ListCell *l; + if (partspec->is_transformed) + return copyObject(partspec); + newspec = makeNode(PartitionSpec); newspec->strategy = partspec->strategy; diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index b3188d87209..4b39e3c7587 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -4612,6 +4612,15 @@ transformPartitionBound(ParseState *pstate, Relation parent, int partnatts = get_partition_natts(key); List *partexprs = get_partition_exprs(key); + /* + * Check if the bound specification is already in transformed form (i.e., + * it was retrieved from pg_class.relpartbound rather than parsed from + * SQL). This happens when copying partitions via CREATE SCHEMA ... LIKE. + * In this case, skip transformation and return a copy of the input. + */ + if (spec->is_transformed) + return copyObject(spec); + /* Avoid scribbling on input */ result_spec = copyObject(spec); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2171c778f84..40a7143aadd 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -916,6 +916,8 @@ typedef struct PartitionSpec PartitionStrategy strategy; List *partParams; /* List of PartitionElems */ ParseLoc location; /* token location, or -1 if unknown */ + + bool is_transformed; } PartitionSpec; /* @@ -928,6 +930,8 @@ struct PartitionBoundSpec { NodeTag type; + bool is_transformed; + char strategy; /* see PARTITION_STRATEGY codes above */ bool is_default; /* is it a default partition bound? */ diff --git a/src/test/regress/expected/create_schema.out b/src/test/regress/expected/create_schema.out index 7aa93deb947..0ecbea18fda 100644 --- a/src/test/regress/expected/create_schema.out +++ b/src/test/regress/expected/create_schema.out @@ -194,6 +194,138 @@ WHERE table_schema = 'regress_copy4' ORDER BY table_name; -- Test source schema does not exist CREATE SCHEMA regress_copy_fail LIKE nonexistent_schema INCLUDING TABLE; ERROR: schema "nonexistent_schema" does not exist +-- +-- Test partitioned tables handling +-- +-- Create a schema with partitioned table and partitions +CREATE SCHEMA regress_part_source; +CREATE TABLE regress_part_source.sales ( + id int, + sale_date date, + amount numeric +) PARTITION BY RANGE (sale_date); +CREATE TABLE regress_part_source.sales2 ( + id SERIAL, + amount NUMERIC, + sale_date DATE +) PARTITION BY RANGE (EXTRACT(YEAR FROM sale_date)); +CREATE TABLE regress_part_source.sales_2025 PARTITION OF regress_part_source.sales2 FOR VALUES FROM (2024) TO (2025); +CREATE TABLE regress_part_source.sales_2026 PARTITION OF regress_part_source.sales2 FOR VALUES FROM (2025) TO (2026); +CREATE TABLE regress_part_source.sales_2023 PARTITION OF regress_part_source.sales + FOR VALUES FROM ('2023-01-01') TO ('2024-01-01'); +CREATE TABLE regress_part_source.sales_2024 PARTITION OF regress_part_source.sales + FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); +-- Copy the schema - partitioned table should preserve partition spec +-- and partitions should be attached +CREATE SCHEMA regress_part_copy LIKE regress_part_source INCLUDING ALL; +-- Verify the partitioned table was created with partition spec and that +-- partitions were attached +\d+ regress_part_copy.sales + Partitioned table "regress_part_copy.sales" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +-----------+---------+-----------+----------+---------+---------+--------------+------------- + id | integer | | | | plain | | + sale_date | date | | | | plain | | + amount | numeric | | | | main | | +Partition key: RANGE (sale_date) +Partitions: regress_part_copy.sales_2023 FOR VALUES FROM ('01-01-2023') TO ('01-01-2024'), + regress_part_copy.sales_2024 FOR VALUES FROM ('01-01-2024') TO ('01-01-2025') + +\d+ regress_part_copy.sales2 + Partitioned table "regress_part_copy.sales2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +-----------+---------+-----------+----------+--------------------------------------------------------+---------+--------------+------------- + id | integer | | not null | nextval('regress_part_source.sales2_id_seq'::regclass) | plain | | + amount | numeric | | | | main | | + sale_date | date | | | | plain | | +Partition key: RANGE (EXTRACT(year FROM sale_date)) +Not-null constraints: + "sales2_id_not_null" NOT NULL "id" +Partitions: regress_part_copy.sales_2025 FOR VALUES FROM ('2024') TO ('2025'), + regress_part_copy.sales_2026 FOR VALUES FROM ('2025') TO ('2026') + +-- Test orphan partitions: parent in different schema +-- Create partition in different schema than its parent +CREATE SCHEMA regress_orphan_source; +CREATE TABLE regress_orphan_source.orphan_part PARTITION OF regress_part_source.sales + FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); +-- Copy the schema with orphan partition - should skip with WARNING +CREATE SCHEMA regress_orphan_copy LIKE regress_orphan_source INCLUDING ALL; +WARNING: skipping partition "regress_orphan_source.orphan_part" because its parent table is in schema "regress_part_source" +-- The orphan partition should NOT be copied (it was skipped) +\d regress_orphan_copy.* +-- Test list partitioning +CREATE SCHEMA regress_list_part_source; +CREATE TABLE regress_list_part_source.products ( + id int, + category text, + name text +) PARTITION BY LIST (category); +CREATE TABLE regress_list_part_source.products_electronics + PARTITION OF regress_list_part_source.products + FOR VALUES IN ('electronics', 'computers'); +CREATE TABLE regress_list_part_source.products_clothing + PARTITION OF regress_list_part_source.products + FOR VALUES IN ('clothing', 'shoes'); +CREATE SCHEMA regress_list_part_copy LIKE regress_list_part_source INCLUDING ALL; +-- Verify list partitioned table structure and that partitions were attached correctly +\d+ regress_list_part_copy.products + Partitioned table "regress_list_part_copy.products" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +----------+---------+-----------+----------+---------+----------+--------------+------------- + id | integer | | | | plain | | + category | text | | | | extended | | + name | text | | | | extended | | +Partition key: LIST (category) +Partitions: regress_list_part_copy.products_clothing FOR VALUES IN ('clothing', 'shoes'), + regress_list_part_copy.products_electronics FOR VALUES IN ('electronics', 'computers') + +-- Test hash partitioning +CREATE SCHEMA regress_hash_part_source; +CREATE TABLE regress_hash_part_source.events ( + id int, + event_type text +) PARTITION BY HASH (id); +CREATE TABLE regress_hash_part_source.events_0 + PARTITION OF regress_hash_part_source.events + FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE regress_hash_part_source.events_1 + PARTITION OF regress_hash_part_source.events + FOR VALUES WITH (MODULUS 2, REMAINDER 1); +CREATE SCHEMA regress_hash_part_copy LIKE regress_hash_part_source INCLUDING ALL; +-- Verify hash partitioned table structure and that partitions were attached correctly +\d+ regress_hash_part_copy.events + Partitioned table "regress_hash_part_copy.events" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +------------+---------+-----------+----------+---------+----------+--------------+------------- + id | integer | | | | plain | | + event_type | text | | | | extended | | +Partition key: HASH (id) +Partitions: regress_hash_part_copy.events_0 FOR VALUES WITH (modulus 2, remainder 0), + regress_hash_part_copy.events_1 FOR VALUES WITH (modulus 2, remainder 1) + +-- Clean up partition tests +DROP SCHEMA regress_part_source CASCADE; +NOTICE: drop cascades to 5 other objects +DETAIL: drop cascades to table regress_part_source.sales +drop cascades to table regress_part_source.sales2 +drop cascades to default value for column id of table regress_part_copy.sales2 +drop cascades to default value for column id of table regress_part_copy.sales_2025 +drop cascades to default value for column id of table regress_part_copy.sales_2026 +DROP SCHEMA regress_part_copy CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table regress_part_copy.sales +drop cascades to table regress_part_copy.sales2 +DROP SCHEMA regress_orphan_source CASCADE; +DROP SCHEMA regress_orphan_copy CASCADE; +DROP SCHEMA regress_list_part_source CASCADE; +NOTICE: drop cascades to table regress_list_part_source.products +DROP SCHEMA regress_list_part_copy CASCADE; +NOTICE: drop cascades to table regress_list_part_copy.products +DROP SCHEMA regress_hash_part_source CASCADE; +NOTICE: drop cascades to table regress_hash_part_source.events +DROP SCHEMA regress_hash_part_copy CASCADE; +NOTICE: drop cascades to table regress_hash_part_copy.events -- Clean up LIKE tests DROP SCHEMA regress_copy1 CASCADE; NOTICE: drop cascades to 3 other objects @@ -211,6 +343,8 @@ DETAIL: drop cascades to table regress_copy3.t1 drop cascades to table regress_copy3.t2 drop cascades to table regress_copy3.t3 DROP SCHEMA regress_copy4 CASCADE; +DROP SCHEMA regress_copy_func CASCADE; +ERROR: schema "regress_copy_func" does not exist DROP SCHEMA regress_empty_source CASCADE; DROP SCHEMA regress_source_schema CASCADE; NOTICE: drop cascades to 3 other objects diff --git a/src/test/regress/sql/create_schema.sql b/src/test/regress/sql/create_schema.sql index a00cf75869d..8102149e5ea 100644 --- a/src/test/regress/sql/create_schema.sql +++ b/src/test/regress/sql/create_schema.sql @@ -131,11 +131,114 @@ WHERE table_schema = 'regress_copy4' ORDER BY table_name; -- Test source schema does not exist CREATE SCHEMA regress_copy_fail LIKE nonexistent_schema INCLUDING TABLE; +-- +-- Test partitioned tables handling +-- + +-- Create a schema with partitioned table and partitions +CREATE SCHEMA regress_part_source; + +CREATE TABLE regress_part_source.sales ( + id int, + sale_date date, + amount numeric +) PARTITION BY RANGE (sale_date); + +CREATE TABLE regress_part_source.sales2 ( + id SERIAL, + amount NUMERIC, + sale_date DATE +) PARTITION BY RANGE (EXTRACT(YEAR FROM sale_date)); + +CREATE TABLE regress_part_source.sales_2025 PARTITION OF regress_part_source.sales2 FOR VALUES FROM (2024) TO (2025); +CREATE TABLE regress_part_source.sales_2026 PARTITION OF regress_part_source.sales2 FOR VALUES FROM (2025) TO (2026); + +CREATE TABLE regress_part_source.sales_2023 PARTITION OF regress_part_source.sales + FOR VALUES FROM ('2023-01-01') TO ('2024-01-01'); + +CREATE TABLE regress_part_source.sales_2024 PARTITION OF regress_part_source.sales + FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); + +-- Copy the schema - partitioned table should preserve partition spec +-- and partitions should be attached +CREATE SCHEMA regress_part_copy LIKE regress_part_source INCLUDING ALL; + +-- Verify the partitioned table was created with partition spec and that +-- partitions were attached +\d+ regress_part_copy.sales +\d+ regress_part_copy.sales2 + +-- Test orphan partitions: parent in different schema +-- Create partition in different schema than its parent +CREATE SCHEMA regress_orphan_source; +CREATE TABLE regress_orphan_source.orphan_part PARTITION OF regress_part_source.sales + FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); + +-- Copy the schema with orphan partition - should skip with WARNING +CREATE SCHEMA regress_orphan_copy LIKE regress_orphan_source INCLUDING ALL; + +-- The orphan partition should NOT be copied (it was skipped) +\d regress_orphan_copy.* + +-- Test list partitioning +CREATE SCHEMA regress_list_part_source; + +CREATE TABLE regress_list_part_source.products ( + id int, + category text, + name text +) PARTITION BY LIST (category); + +CREATE TABLE regress_list_part_source.products_electronics + PARTITION OF regress_list_part_source.products + FOR VALUES IN ('electronics', 'computers'); + +CREATE TABLE regress_list_part_source.products_clothing + PARTITION OF regress_list_part_source.products + FOR VALUES IN ('clothing', 'shoes'); + +CREATE SCHEMA regress_list_part_copy LIKE regress_list_part_source INCLUDING ALL; + +-- Verify list partitioned table structure and that partitions were attached correctly +\d+ regress_list_part_copy.products + +-- Test hash partitioning +CREATE SCHEMA regress_hash_part_source; + +CREATE TABLE regress_hash_part_source.events ( + id int, + event_type text +) PARTITION BY HASH (id); + +CREATE TABLE regress_hash_part_source.events_0 + PARTITION OF regress_hash_part_source.events + FOR VALUES WITH (MODULUS 2, REMAINDER 0); + +CREATE TABLE regress_hash_part_source.events_1 + PARTITION OF regress_hash_part_source.events + FOR VALUES WITH (MODULUS 2, REMAINDER 1); + +CREATE SCHEMA regress_hash_part_copy LIKE regress_hash_part_source INCLUDING ALL; + +-- Verify hash partitioned table structure and that partitions were attached correctly +\d+ regress_hash_part_copy.events + +-- Clean up partition tests +DROP SCHEMA regress_part_source CASCADE; +DROP SCHEMA regress_part_copy CASCADE; +DROP SCHEMA regress_orphan_source CASCADE; +DROP SCHEMA regress_orphan_copy CASCADE; +DROP SCHEMA regress_list_part_source CASCADE; +DROP SCHEMA regress_list_part_copy CASCADE; +DROP SCHEMA regress_hash_part_source CASCADE; +DROP SCHEMA regress_hash_part_copy CASCADE; + -- Clean up LIKE tests DROP SCHEMA regress_copy1 CASCADE; DROP SCHEMA regress_copy2 CASCADE; DROP SCHEMA regress_copy3 CASCADE; DROP SCHEMA regress_copy4 CASCADE; +DROP SCHEMA regress_copy_func CASCADE; DROP SCHEMA regress_empty_source CASCADE; DROP SCHEMA regress_source_schema CASCADE; -- 2.52.0